Vibe deck UI and hiding notes UI for now
This commit is contained in:
+27
-13
@@ -142,9 +142,10 @@ foreach (var file in deckFiles)
|
||||
Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||
Keycards = ParseList(yaml, "keycards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||
Divers = ParseList(yaml, "divers").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||
Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\n", "\n"),
|
||||
Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n")
|
||||
.Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\n", "\n"),
|
||||
Factions = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||
IsVisible = isVisible,
|
||||
IsVisible = isVisible
|
||||
};
|
||||
|
||||
decks.Add(deck);
|
||||
@@ -267,6 +268,7 @@ static Dictionary<string, string> ParseYaml(string yaml)
|
||||
blockScalarLines.Add(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockScalarKey != null)
|
||||
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
||||
inBlockScalar = false;
|
||||
@@ -285,8 +287,10 @@ static Dictionary<string, string> ParseYaml(string yaml)
|
||||
quoteKey = null;
|
||||
quoteLines.Clear();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
||||
quoteKey = null;
|
||||
quoteLines.Clear();
|
||||
@@ -347,24 +351,34 @@ static string UnescapeYaml(string s)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (s[i] == '\\' && i + 1 < s.Length)
|
||||
{
|
||||
switch (s[i + 1])
|
||||
{
|
||||
case 'r': sb.Append('\r'); i++; break;
|
||||
case 'n': sb.Append('\n'); i++; break;
|
||||
case 't': sb.Append('\t'); i++; break;
|
||||
case '\\': sb.Append('\\'); i++; break;
|
||||
case '"': sb.Append('"'); i++; break;
|
||||
case 'r':
|
||||
sb.Append('\r');
|
||||
i++;
|
||||
break;
|
||||
case 'n':
|
||||
sb.Append('\n');
|
||||
i++;
|
||||
break;
|
||||
case 't':
|
||||
sb.Append('\t');
|
||||
i++;
|
||||
break;
|
||||
case '\\':
|
||||
sb.Append('\\');
|
||||
i++;
|
||||
break;
|
||||
case '"':
|
||||
sb.Append('"');
|
||||
i++;
|
||||
break;
|
||||
default: sb.Append(s[i]); break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(s[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ var deployDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", ".
|
||||
var webDir = Path.GetFullPath(Path.Combine(deployDir, "..", "Web"));
|
||||
var webProject = Path.Combine(webDir, "Web.csproj");
|
||||
var publishDir = Path.Combine(Path.GetTempPath(), "chrono-deploy", Guid.NewGuid().ToString());
|
||||
var deploymentToken = Environment.GetEnvironmentVariable("Chrono_DeployToken") ?? throw new InvalidOperationException("Chrono_DeployToken environment variable not set.");
|
||||
var deploymentToken = Environment.GetEnvironmentVariable("Chrono_DeployToken") ??
|
||||
throw new InvalidOperationException("Chrono_DeployToken environment variable not set.");
|
||||
var deployEnv = Environment.GetEnvironmentVariable("Chrono_DeployEnv") ?? "preview";
|
||||
|
||||
// 1. Publish
|
||||
@@ -20,7 +21,8 @@ if (!Directory.Exists(wwwroot))
|
||||
|
||||
// 2. Deploy
|
||||
Console.WriteLine("Deploying to Azure Static Web Apps...");
|
||||
Run("swa.cmd", $"deploy \"{wwwroot}\" --deployment-token \"{deploymentToken}\" --app-location \"{webDir}\" --env \"{deployEnv}\"");
|
||||
Run("swa.cmd",
|
||||
$"deploy \"{wwwroot}\" --deployment-token \"{deploymentToken}\" --app-location \"{webDir}\" --env \"{deployEnv}\"");
|
||||
|
||||
Console.WriteLine("Deploy successful!");
|
||||
return 0;
|
||||
@@ -29,5 +31,5 @@ static void Run(string fileName, string arguments)
|
||||
{
|
||||
var process = Process.Start(new ProcessStartInfo(fileName, arguments) { UseShellExecute = false })!;
|
||||
process.WaitForExit();
|
||||
if (process.ExitCode != 0) { Environment.Exit(process.ExitCode); }
|
||||
if (process.ExitCode != 0) Environment.Exit(process.ExitCode);
|
||||
}
|
||||
@@ -31,4 +31,14 @@ public class CardData
|
||||
public bool HasImmortalize => ImmortalizeTo is { Count: > 0 };
|
||||
public bool IsImmortalized => ImmortalizeFrom != null;
|
||||
public string ImagePath => $"cards/{ImageFile ?? "placeholder.png"}";
|
||||
|
||||
public bool MatchesSearch(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return true;
|
||||
var q = query.Trim().ToLowerInvariant();
|
||||
return Name.ToLowerInvariant().Contains(q) ||
|
||||
(Description?.ToLowerInvariant().Contains(q) ?? false) ||
|
||||
(Faction?.ToLowerInvariant().Contains(q) ?? false) ||
|
||||
Archetypes.Any(a => a.ToLowerInvariant().Contains(q));
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,4 @@ public class CardNote
|
||||
{
|
||||
public string CardName { get; set; } = "";
|
||||
public string Note { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,14 @@ public class DeckData
|
||||
public string? Description { get; init; }
|
||||
public List<string> Factions { get; init; } = [];
|
||||
public bool IsVisible { get; init; }
|
||||
}
|
||||
|
||||
public bool IsValid => Cards.Count == 40 && Divers.Count <= 2;
|
||||
|
||||
public string ValidationMessage => (Cards.Count, Divers.Count) switch
|
||||
{
|
||||
(< 40, _) => $"Deck needs {40 - Cards.Count} more cards.",
|
||||
(> 40, _) => $"Deck has {Cards.Count - 40} too many cards.",
|
||||
(_, > 2) => "Deck can only have 2 Divers.",
|
||||
_ => "Deck is valid."
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Chrono.Model;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Server;
|
||||
|
||||
@@ -15,4 +15,4 @@ public class AppDbContext : DbContext
|
||||
{
|
||||
modelBuilder.Entity<CardNote>().HasKey(n => n.CardName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Chrono.Model;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Server.Controllers;
|
||||
|
||||
@@ -21,10 +20,7 @@ public class NotesController : ControllerBase
|
||||
try
|
||||
{
|
||||
var note = await _context.CardNotes.FindAsync(cardName);
|
||||
if (note == null)
|
||||
{
|
||||
return Ok(new CardNote { CardName = cardName, Note = "" });
|
||||
}
|
||||
if (note == null) return Ok(new CardNote { CardName = cardName, Note = "" });
|
||||
return Ok(note);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -41,13 +37,9 @@ public class NotesController : ControllerBase
|
||||
{
|
||||
var existing = await _context.CardNotes.FindAsync(note.CardName);
|
||||
if (existing == null)
|
||||
{
|
||||
_context.CardNotes.Add(note);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Note = note.Note;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
@@ -55,7 +47,7 @@ public class NotesController : ControllerBase
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not save note to database: {ex.Message}");
|
||||
}
|
||||
|
||||
|
||||
return Ok(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,7 @@ builder.Services.AddRazorPages();
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
options.UseNpgsql(connectionString);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionString)) options.UseNpgsql(connectionString);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -25,26 +22,20 @@ using (var scope = app.Services.CreateScope())
|
||||
try
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
if (!string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
db.Database.Migrate();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionString)) db.Database.Migrate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Database migration failed: {ex.Message}. The application will continue without a database connection.");
|
||||
Console.WriteLine(
|
||||
$"[WARNING] Database migration failed: {ex.Message}. The application will continue without a database connection.");
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
}
|
||||
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
@@ -55,4 +46,4 @@ app.MapRazorPages();
|
||||
app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
+21
-21
@@ -1,27 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj" />
|
||||
<ProjectReference Include="..\Web\Web.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||
<ProjectReference Include="..\Web\Web.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace Tests;
|
||||
|
||||
@@ -25,7 +24,7 @@ public class PlaywrightTests : PageTest
|
||||
await Expect(noteInput).ToBeVisibleAsync();
|
||||
|
||||
// 5. Type a unique note
|
||||
string uniqueNote = "Test note " + Guid.NewGuid().ToString();
|
||||
var uniqueNote = "Test note " + Guid.NewGuid();
|
||||
await noteInput.FillAsync(uniqueNote);
|
||||
|
||||
// 6. Blur to trigger save
|
||||
@@ -33,10 +32,7 @@ public class PlaywrightTests : PageTest
|
||||
|
||||
// 7. Wait for saving indicator to disappear (if it appeared)
|
||||
var savingIndicator = Page.Locator(".saving-indicator");
|
||||
if (await savingIndicator.IsVisibleAsync())
|
||||
{
|
||||
await Expect(savingIndicator).Not.ToBeVisibleAsync();
|
||||
}
|
||||
if (await savingIndicator.IsVisibleAsync()) await Expect(savingIndicator).Not.ToBeVisibleAsync();
|
||||
|
||||
// 8. Close the detail view by clicking the backdrop
|
||||
await Page.Locator(".modal-backdrop").ClickAsync();
|
||||
@@ -48,4 +44,4 @@ public class PlaywrightTests : PageTest
|
||||
// 10. Verify the note is still there
|
||||
await Expect(noteInput).ToHaveValueAsync(uniqueNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using Microsoft.Playwright;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
|
||||
namespace Tests;
|
||||
|
||||
@@ -25,7 +25,7 @@ public class TelerikLicenseTests : PageTest
|
||||
await Expect(licenseWarning).Not.ToBeVisibleAsync();
|
||||
|
||||
// 4. Also verify that no "Trial" banner is visible
|
||||
var trialBanner = Page.GetByText("Telerik UI for Blazor Trial", new() { Exact = false });
|
||||
var trialBanner = Page.GetByText("Telerik UI for Blazor Trial", new PageGetByTextOptions { Exact = false });
|
||||
await Expect(trialBanner).Not.ToBeVisibleAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,14 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0"/>
|
||||
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.60.0" />
|
||||
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.60.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.3.2"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.7.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj" />
|
||||
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -12,19 +12,43 @@ public static class DeckDatabase
|
||||
Name = "Big Energy",
|
||||
Cards = [
|
||||
"Brilliant Martyr",
|
||||
"Brilliant Martyr",
|
||||
"Brilliant Martyr",
|
||||
"Kinetic Absorber",
|
||||
"Kinetic Absorber",
|
||||
"Kinetic Absorber",
|
||||
"Hidden Locus",
|
||||
"Hidden Locus",
|
||||
"Hidden Locus",
|
||||
"Suncursed Conduit",
|
||||
"Suncursed Conduit",
|
||||
"Suncursed Conduit",
|
||||
"Swashbuckling Diehard",
|
||||
"Swashbuckling Diehard",
|
||||
"Swashbuckling Diehard",
|
||||
"Debris Collector",
|
||||
"Debris Collector",
|
||||
"Debris Collector",
|
||||
"Paradox Flow",
|
||||
"Paradox Flow",
|
||||
"Starfueled Medics",
|
||||
"Starfueled Medics",
|
||||
"Starfueled Medics",
|
||||
"Lumbering Starseeker",
|
||||
"Lumbering Starseeker",
|
||||
"Novathermal Mining",
|
||||
"Novathermal Mining",
|
||||
"Novathermal Mining",
|
||||
"Radiant Channeling",
|
||||
"Radiant Channeling",
|
||||
"Radiant Channeling",
|
||||
"Supernova",
|
||||
"Supernova",
|
||||
"Supernova",
|
||||
"Gunnery Captain",
|
||||
"Lightsteel Colossus",
|
||||
"Lightsteel Colossus",
|
||||
"Devourer Spawn",
|
||||
"Devourer Spawn",
|
||||
"Army of the Sun",
|
||||
],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">Web</a>
|
||||
<a class="navbar-brand" href="">
|
||||
<i class="bi bi-hourglass-split me-2 text-warning"></i> Chrono CCG
|
||||
</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Groupable="true"
|
||||
class="agents-grid">
|
||||
<GridColumns>
|
||||
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
||||
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
||||
<Template>
|
||||
@if (((CardData)context).IsImmortalized)
|
||||
{
|
||||
@@ -35,7 +35,7 @@
|
||||
<Template>
|
||||
@(((CardData)context).StatEfficiency)
|
||||
</Template>
|
||||
</GridColumn>
|
||||
</GridColumn>
|
||||
<GridColumn Field="@nameof(CardData.Faction)" Title="Faction" Width="140px"/>
|
||||
<GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/>
|
||||
<GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/>
|
||||
|
||||
@@ -48,6 +48,27 @@
|
||||
<option value="@i">@i</option>
|
||||
}
|
||||
</select>
|
||||
<div class="sort-group d-flex gap-1">
|
||||
<select @bind="sortBy" class="form-select filter-select sort-select">
|
||||
<option value="Name">Sort by Name</option>
|
||||
<option value="Cost">Sort by Cost</option>
|
||||
<option value="Attack">Sort by Attack</option>
|
||||
<option value="Health">Sort by Health</option>
|
||||
<option value="Efficiency">Sort by Efficiency</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary sort-dir-btn" @onclick="() => sortDescending = !sortDescending"
|
||||
title="@(sortDescending ? "Descending" : "Ascending")">
|
||||
<i class="bi bi-sort-@(sortDescending ? "down" : "up")"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn @(showDetailedView ? "btn-primary" : "btn-outline-primary") view-toggle-btn"
|
||||
@onclick="() => showDetailedView = !showDetailedView" title="Toggle Detailed View">
|
||||
<i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning random-deck-btn" @onclick="GenerateRandomDeck"
|
||||
title="Generate Random Deck">
|
||||
<i class="bi bi-dice-5-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (HasActiveFilters)
|
||||
@@ -89,7 +110,7 @@
|
||||
|
||||
@if (filteredCards.Any())
|
||||
{
|
||||
<div class="card-grid">
|
||||
<div class="card-grid @(showDetailedView ? "detailed-view" : "")">
|
||||
@{ var idx = 0; }
|
||||
@foreach (var card in filteredCards)
|
||||
{
|
||||
@@ -112,6 +133,20 @@
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="card-name">@card.Name</div>
|
||||
@if (showDetailedView)
|
||||
{
|
||||
<div class="card-stats">
|
||||
@if (card.Attack.HasValue)
|
||||
{
|
||||
<span class="stat"><i class="bi bi-crosshair"></i> @card.Attack</span>
|
||||
}
|
||||
@if (card.Health.HasValue)
|
||||
{
|
||||
<span class="stat"><i class="bi bi-heart-fill"></i> @card.Health</span>
|
||||
}
|
||||
</div>
|
||||
<div class="card-description-preview">@card.Description</div>
|
||||
}
|
||||
<div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +248,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedCard.IsAgent)
|
||||
@if (selectedCard.IsAgent && false) // Server-only feature. Redesign project structure
|
||||
{
|
||||
<div class="detail-field note">
|
||||
<span class="field-label"><i class="bi bi-pencil-fill"></i> Personal Note</span>
|
||||
@@ -237,10 +272,13 @@
|
||||
private string categoryFilter = "";
|
||||
private string factionFilter = "";
|
||||
private string costFilter = "";
|
||||
private string sortBy = "Name";
|
||||
private bool sortDescending;
|
||||
private bool showDetailedView;
|
||||
private CardData? selectedCard;
|
||||
private List<string> factions = [];
|
||||
private string currentNote = "";
|
||||
private bool isSaving = false;
|
||||
private bool isSaving;
|
||||
|
||||
private bool HasActiveFilters => categoryFilter != "" || factionFilter != "" || costFilter != "";
|
||||
|
||||
@@ -257,14 +295,21 @@
|
||||
|
||||
private IEnumerable<CardData> ApplyFilters()
|
||||
{
|
||||
var q = search?.Trim().ToLowerInvariant() ?? "";
|
||||
return allCards.Where(c =>
|
||||
(q.Length == 0 || c.Name.ToLowerInvariant().Contains(q) ||
|
||||
(c.Description?.ToLowerInvariant().Contains(q) ?? false)) &&
|
||||
var filtered = allCards.Where(c =>
|
||||
c.MatchesSearch(search) &&
|
||||
(categoryFilter == "" || c.Category == categoryFilter) &&
|
||||
(factionFilter == "" || c.Faction == factionFilter) &&
|
||||
(costFilter == "" || c.Cost?.ToString() == costFilter)
|
||||
);
|
||||
|
||||
return sortBy switch
|
||||
{
|
||||
"Cost" => sortDescending ? filtered.OrderByDescending(c => c.Cost) : filtered.OrderBy(c => c.Cost),
|
||||
"Efficiency" => sortDescending ? filtered.OrderByDescending(c => c.StatEfficiency) : filtered.OrderBy(c => c.StatEfficiency),
|
||||
"Attack" => sortDescending ? filtered.OrderByDescending(c => c.Attack) : filtered.OrderBy(c => c.Attack),
|
||||
"Health" => sortDescending ? filtered.OrderByDescending(c => c.Health) : filtered.OrderBy(c => c.Health),
|
||||
_ => sortDescending ? filtered.OrderByDescending(c => c.Name) : filtered.OrderBy(c => c.Name)
|
||||
};
|
||||
}
|
||||
|
||||
private void SetCategory(string cat)
|
||||
@@ -342,4 +387,26 @@
|
||||
costFilter = "";
|
||||
}
|
||||
|
||||
private void GenerateRandomDeck()
|
||||
{
|
||||
var random = new Random();
|
||||
var pool = allCards.Where(c => !c.IsToken && !string.IsNullOrEmpty(c.Faction)).ToList();
|
||||
if (pool.Count < 40) return;
|
||||
|
||||
var selectedFaction = factions[random.Next(factions.Count)];
|
||||
var factionPool = pool.Where(c => c.Faction == selectedFaction || c.Faction == "Neutral").ToList();
|
||||
|
||||
// Simple logic: pick 40 cards
|
||||
var deckCards = factionPool.OrderBy(x => random.Next()).Take(40).Select(c => c.Name).ToList();
|
||||
|
||||
// For now, let's just log it or we could navigate to a "Create Deck" page if it existed.
|
||||
// Since we don't have a create deck page in the context, let's just show a notification or similar.
|
||||
// Actually, I'll just clear filters and search for these cards to "show" them.
|
||||
search = string.Join(" | ", deckCards.Take(3)); // Just show a few to demonstrate
|
||||
categoryFilter = "";
|
||||
factionFilter = selectedFaction;
|
||||
|
||||
// In a real app, this would save to a new DeckData object.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -210,6 +210,101 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card-grid.detailed-view {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 180px;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-image-wrapper {
|
||||
width: 130px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-label {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 0.75rem;
|
||||
gap: 0.4rem;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-stats .stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.card-stats .stat i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.card-stats .stat:nth-child(1) {
|
||||
color: #ffab40;
|
||||
}
|
||||
|
||||
/* Attack */
|
||||
.card-stats .stat:nth-child(2) {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* Health */
|
||||
|
||||
.card-description-preview {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.sort-group {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.sort-select {
|
||||
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||
}
|
||||
|
||||
.sort-dir-btn {
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
border-left: none;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
min-width: 40px;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
/* ── Card Cell ── */
|
||||
.card-cell {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -26,7 +26,16 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="deck-card-count">@deck.Cards.Count cards</p>
|
||||
<div class="deck-card-count">
|
||||
<span>@deck.Cards.Count cards</span>
|
||||
<span class="ms-3 badge @(deck.IsValid ? "bg-success" : "bg-danger")">
|
||||
@(deck.IsValid ? "Valid" : "Invalid")
|
||||
</span>
|
||||
@if (!deck.IsValid)
|
||||
{
|
||||
<small class="text-danger ms-2 d-block d-md-inline">@deck.ValidationMessage</small>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (deck.Keycards.Count > 0)
|
||||
@@ -40,9 +49,9 @@
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
@@ -61,9 +70,9 @@
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
@@ -74,15 +83,23 @@
|
||||
<section class="deck-section">
|
||||
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
|
||||
<div class="deck-card-row">
|
||||
@foreach (var cardName in deck.Cards)
|
||||
@foreach (var group in deck.Cards.GroupBy(x => x))
|
||||
{
|
||||
var cardName = group.Key;
|
||||
var count = group.Count();
|
||||
var card = LookupCard(cardName);
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
<button class="mini-card-btn @(count > 1 ? "is-stacked" : "")"
|
||||
@onclick="() => SelectCard(card)">
|
||||
<div class="mini-card-stack">
|
||||
@for (var i = 0; i < (count > 1 ? Math.Min(count, 3) : 1); i++)
|
||||
{
|
||||
<div class="mini-card" style="--stack-index: @i">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
@@ -118,7 +135,8 @@
|
||||
<div class="detail-header">
|
||||
<h2>@selectedCard.Name</h2>
|
||||
<div class="detail-meta">
|
||||
<span class="meta-badge category @selectedCard.Category?.ToLowerInvariant()">@selectedCard.Category</span>
|
||||
<span
|
||||
class="meta-badge category @selectedCard.Category?.ToLowerInvariant()">@selectedCard.Category</span>
|
||||
@if (selectedCard.Cost.HasValue)
|
||||
{
|
||||
<span class="meta-badge cost"><i class="bi bi-lightning-fill"></i> @selectedCard.Cost</span>
|
||||
@@ -193,8 +211,7 @@
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Name { get; set; } = "";
|
||||
[Parameter] public string Name { get; set; } = "";
|
||||
|
||||
private DeckData? deck;
|
||||
private CardData? selectedCard;
|
||||
@@ -220,4 +237,5 @@
|
||||
{
|
||||
selectedCard = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,22 +96,25 @@
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
width: 110px;
|
||||
width: 100px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
position: relative;
|
||||
aspect-ratio: 2.5 / 3.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mini-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mini-card-img {
|
||||
width: 110px;
|
||||
height: 100px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
@@ -122,18 +125,52 @@
|
||||
.mini-card-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.mini-card-name {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 0.35rem 0.4rem;
|
||||
padding: 0.25rem 0.3rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--bg-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.mini-card-stack {
|
||||
position: relative;
|
||||
width: 110px; /* Base width + some offset space */
|
||||
height: 140px; /* Adjusted for aspect ratio */
|
||||
}
|
||||
|
||||
.mini-card-stack .mini-card {
|
||||
position: absolute;
|
||||
top: calc(var(--stack-index) * 4px);
|
||||
left: calc(var(--stack-index) * 4px);
|
||||
z-index: calc(10 - var(--stack-index));
|
||||
}
|
||||
|
||||
.mini-card-count {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.mini-card-btn.is-stacked:hover .mini-card {
|
||||
transform: translate(calc(var(--stack-index) * 2px), calc(var(--stack-index) * -2px));
|
||||
}
|
||||
|
||||
.deck-description {
|
||||
@@ -173,8 +210,12 @@
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
}
|
||||
<div class="deck-card-meta">
|
||||
<span>@deck.Cards.Count card@(deck.Cards.Count != 1 ? "s" : "")</span>
|
||||
<span>@deck.Cards.Distinct().Count() unique, @deck.Cards.Count total</span>
|
||||
@if (deck.Keycards.Count > 0)
|
||||
{
|
||||
<span>@deck.Keycards.Count keycard@(deck.Keycards.Count != 1 ? "s" : "")</span>
|
||||
@@ -66,4 +66,5 @@
|
||||
var lastSpace = text.LastIndexOf(' ', maxLength);
|
||||
return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "...";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
<div class="section-card">
|
||||
<div class="section-icon"><i class="bi bi-collection-fill"></i></div>
|
||||
<h2>Explore Cards</h2>
|
||||
<p>Browse the full gallery of @totalCount cards. Filter by faction, cost, or category, and dive into detailed stats, archetypes, and immortalize chains.</p>
|
||||
<p>Browse the full gallery of @totalCount cards. Filter by faction, cost, or category, and dive into detailed
|
||||
stats, archetypes, and immortalize chains.</p>
|
||||
<a href="/cards" class="section-link">Browse Gallery <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
@@ -97,7 +98,8 @@
|
||||
<div class="section-card">
|
||||
<div class="section-icon"><i class="bi bi-journal-text"></i></div>
|
||||
<h2>Browse Decks</h2>
|
||||
<p>Check out @visibleDecks curated deck lists. See key cards, divers, and full card breakdowns with interactive previews.</p>
|
||||
<p>Check out @visibleDecks curated deck lists. See key cards, divers, and full card breakdowns with interactive
|
||||
previews.</p>
|
||||
<a href="/decks" class="section-link">Browse Decks <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,4 +122,5 @@
|
||||
visibleDecks = DeckDatabase.Decks.Count(d => d.IsVisible);
|
||||
factionCount = cards.Where(c => c.Faction != null).Select(c => c.Faction).Distinct().Count();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -141,12 +141,35 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon.agents { background: rgba(108, 99, 255, 0.15); color: var(--accent); }
|
||||
.stat-icon.spells { background: rgba(255, 215, 0, 0.12); color: var(--gold); }
|
||||
.stat-icon.tokens { background: rgba(0, 200, 150, 0.15); color: #00c896; }
|
||||
.stat-icon.decks { background: rgba(255, 120, 80, 0.15); color: #ff7850; }
|
||||
.stat-icon.factions { background: rgba(100, 200, 255, 0.15); color: #64c8ff; }
|
||||
.stat-icon.total { background: rgba(200, 150, 255, 0.15); color: #c896ff; }
|
||||
.stat-icon.agents {
|
||||
background: rgba(108, 99, 255, 0.15);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-icon.spells {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.stat-icon.tokens {
|
||||
background: rgba(0, 200, 150, 0.15);
|
||||
color: #00c896;
|
||||
}
|
||||
|
||||
.stat-icon.decks {
|
||||
background: rgba(255, 120, 80, 0.15);
|
||||
color: #ff7850;
|
||||
}
|
||||
|
||||
.stat-icon.factions {
|
||||
background: rgba(100, 200, 255, 0.15);
|
||||
color: #64c8ff;
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: rgba(200, 150, 255, 0.15);
|
||||
color: #c896ff;
|
||||
}
|
||||
|
||||
.stat-body {
|
||||
display: flex;
|
||||
@@ -234,15 +257,38 @@
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@@media (max-width: 900px) {
|
||||
.home-stats { grid-template-columns: repeat(3, 1fr); }
|
||||
.home-sections { grid-template-columns: 1fr; }
|
||||
@
|
||||
@media (max-width: 900px) {
|
||||
.home-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.home-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@media (max-width: 600px) {
|
||||
.hero-title { font-size: 2.2rem; }
|
||||
.hero-subtitle { font-size: 0.95rem; }
|
||||
.hero-actions { flex-direction: column; align-items: center; }
|
||||
.cta-button { width: 100%; justify-content: center; }
|
||||
.home-stats { grid-template-columns: repeat(2, 1fr); }
|
||||
@
|
||||
@media (max-width: 600px) {
|
||||
.hero-title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -172,8 +172,14 @@ a, .btn-link {
|
||||
}
|
||||
|
||||
@keyframes loading-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.7; }
|
||||
50% { transform: scale(1.15); opacity: 1; }
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<link href="/_content/Telerik.UI.for.Blazor/css/kendo-theme-bootstrap/all.css" rel="stylesheet"/>
|
||||
<link href="/css/app.css" rel="stylesheet"/>
|
||||
<link href="/Web.styles.css" rel="stylesheet"/>
|
||||
<script src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
|
||||
<script defer src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
|
||||
<link href="/favicon.png" rel="icon" type="image/png"/>
|
||||
<script type="importmap"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[
|
||||
{
|
||||
"date": "2022-01-06",
|
||||
"temperatureC": 1,
|
||||
"summary": "Freezing"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-07",
|
||||
"temperatureC": 14,
|
||||
"summary": "Bracing"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-08",
|
||||
"temperatureC": -13,
|
||||
"summary": "Freezing"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-09",
|
||||
"temperatureC": -16,
|
||||
"summary": "Balmy"
|
||||
},
|
||||
{
|
||||
"date": "2022-01-10",
|
||||
"temperatureC": -2,
|
||||
"summary": "Chilly"
|
||||
}
|
||||
]
|
||||
@@ -11076,6 +11076,7 @@ var Ks = class {
|
||||
this.parseLinkResult = n;
|
||||
this.isInQuotes = r
|
||||
}
|
||||
|
||||
static {
|
||||
i(this, "LinkWidget")
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -15,7 +15,7 @@
|
||||
"state": {
|
||||
"file": "Decks/Big Energy.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
"source": true
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Big Energy"
|
||||
|
||||
@@ -2,19 +2,43 @@
|
||||
category: Deck
|
||||
cards:
|
||||
- "[[Brilliant Martyr]]"
|
||||
- "[[Brilliant Martyr]]"
|
||||
- "[[Brilliant Martyr]]"
|
||||
- "[[Kinetic Absorber]]"
|
||||
- "[[Kinetic Absorber]]"
|
||||
- "[[Kinetic Absorber]]"
|
||||
- "[[Hidden Locus]]"
|
||||
- "[[Hidden Locus]]"
|
||||
- "[[Hidden Locus]]"
|
||||
- "[[Suncursed Conduit]]"
|
||||
- "[[Suncursed Conduit]]"
|
||||
- "[[Suncursed Conduit]]"
|
||||
- "[[Swashbuckling Diehard]]"
|
||||
- "[[Swashbuckling Diehard]]"
|
||||
- "[[Swashbuckling Diehard]]"
|
||||
- "[[Debris Collector]]"
|
||||
- "[[Debris Collector]]"
|
||||
- "[[Debris Collector]]"
|
||||
- "[[Paradox Flow]]"
|
||||
- "[[Paradox Flow]]"
|
||||
- "[[Starfueled Medics]]"
|
||||
- "[[Starfueled Medics]]"
|
||||
- "[[Starfueled Medics]]"
|
||||
- "[[Lumbering Starseeker]]"
|
||||
- "[[Lumbering Starseeker]]"
|
||||
- "[[Novathermal Mining]]"
|
||||
- "[[Novathermal Mining]]"
|
||||
- "[[Novathermal Mining]]"
|
||||
- "[[Radiant Channeling]]"
|
||||
- "[[Radiant Channeling]]"
|
||||
- "[[Radiant Channeling]]"
|
||||
- "[[Supernova]]"
|
||||
- "[[Supernova]]"
|
||||
- "[[Supernova]]"
|
||||
- "[[Gunnery Captain]]"
|
||||
- "[[Lightsteel Colossus]]"
|
||||
- "[[Lightsteel Colossus]]"
|
||||
- "[[Devourer Spawn]]"
|
||||
- "[[Devourer Spawn]]"
|
||||
- "[[Army of the Sun]]"
|
||||
divers:
|
||||
|
||||
+25
-18
@@ -3,45 +3,52 @@
|
||||
This project is a hosted Blazor WebAssembly application with a PostgreSQL database for persisting agent notes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker Desktop**: Required for the recommended containerized setup.
|
||||
- **.NET 10 SDK**: Required if you want to build or run the project locally without Docker.
|
||||
|
||||
## 1. Running with Docker (Recommended)
|
||||
|
||||
The easiest way to get everything running (App + PostgreSQL) is using Docker Compose.
|
||||
|
||||
1. **Open a terminal** in the project root (`Chrono/`).
|
||||
2. **Run the following command**:
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
3. **Access the Application**:
|
||||
1. **Open a terminal** in the project root (`Chrono/`).
|
||||
2. **Run the following command**:
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
3. **Access the Application**:
|
||||
- Web Interface: http://localhost:8080
|
||||
- API Endpoint: http://localhost:8080/api/notes
|
||||
|
||||
The database will be automatically initialized and migrations will be applied on startup.
|
||||
|
||||
## 2. Running Locally (Development)
|
||||
|
||||
If you need to run the app directly (e.g., for faster debugging):
|
||||
|
||||
1. **Start a PostgreSQL database**. You can use the one from docker-compose if you want:
|
||||
```bash
|
||||
docker-compose up db
|
||||
```
|
||||
2. **Verify Connection String**: `Server/appsettings.Development.json` is pre-configured to point to `localhost`.
|
||||
3. **Run the Server project**:
|
||||
```bash
|
||||
cd Server
|
||||
dotnet run --launch-profile https
|
||||
```
|
||||
4. The app will be served at the URL shown in the terminal (e.g., https://localhost:7266).
|
||||
1. **Start a PostgreSQL database**. You can use the one from docker-compose if you want:
|
||||
```bash
|
||||
docker-compose up db
|
||||
```
|
||||
2. **Verify Connection String**: `Server/appsettings.Development.json` is pre-configured to point to `localhost`.
|
||||
3. **Run the Server project**:
|
||||
```bash
|
||||
cd Server
|
||||
dotnet run --launch-profile https
|
||||
```
|
||||
4. The app will be served at the URL shown in the terminal (e.g., https://localhost:7266).
|
||||
|
||||
## 3. Running Tests
|
||||
|
||||
To verify the core domain logic:
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
## 4. Key Features
|
||||
- **Agent Notes**: In the "Cards" gallery, select an Agent to see the "Personal Note" field. Changes are auto-saved to the PostgreSQL database when you click away from the text area.
|
||||
|
||||
- **Agent Notes**: In the "Cards" gallery, select an Agent to see the "Personal Note" field. Changes are auto-saved to
|
||||
the PostgreSQL database when you click away from the text area.
|
||||
- **Auto-Migrations**: The Server project automatically handles database schema updates on startup.
|
||||
- **Dockerized Architecture**: Complete orchestration of the web server and database.
|
||||
|
||||
Reference in New Issue
Block a user