Compare commits
17 Commits
50f3d36083
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2553e98649 | |||
| be34a0bd4a | |||
| 900070c834 | |||
| b36817dd7a | |||
| cf999c883b | |||
| a4ef600f27 | |||
| 6eb314a3a4 | |||
| 7dfff6c2c1 | |||
| d9a9263d46 | |||
| a01397941e | |||
| 26899fb59f | |||
| ce5b45fe46 | |||
| 7978adbd37 | |||
| 9509a24743 | |||
| 3709856e85 | |||
| 66e3da7c9a | |||
| add734b522 |
+43
-19
@@ -17,7 +17,7 @@ if (!Directory.Exists(docsDir))
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdFiles = Directory.GetFiles(docsDir, "*.md");
|
var mdFiles = Directory.GetFiles(docsDir, "*.md", SearchOption.AllDirectories);
|
||||||
var cards = new List<CardData>();
|
var cards = new List<CardData>();
|
||||||
|
|
||||||
foreach (var file in mdFiles)
|
foreach (var file in mdFiles)
|
||||||
@@ -35,7 +35,7 @@ foreach (var file in mdFiles)
|
|||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(file);
|
var name = Path.GetFileNameWithoutExtension(file);
|
||||||
var category = yaml.GetValueOrDefault("category");
|
var category = yaml.GetValueOrDefault("category");
|
||||||
if (category == null) continue;
|
if (category == null || category == "Deck") continue;
|
||||||
|
|
||||||
var imageFile = StripWikiLink(yaml.GetValueOrDefault("imageLink"));
|
var imageFile = StripWikiLink(yaml.GetValueOrDefault("imageLink"));
|
||||||
if (imageFile != null && !imageFile.EndsWith(".png"))
|
if (imageFile != null && !imageFile.EndsWith(".png"))
|
||||||
@@ -67,13 +67,20 @@ foreach (var file in mdFiles)
|
|||||||
// Copy PNGs to wwwroot/cards
|
// Copy PNGs to wwwroot/cards
|
||||||
var cardsDir = Path.Combine(webWwwRoot, "cards");
|
var cardsDir = Path.Combine(webWwwRoot, "cards");
|
||||||
Directory.CreateDirectory(cardsDir);
|
Directory.CreateDirectory(cardsDir);
|
||||||
|
|
||||||
|
var pngFiles = Directory.GetFiles(docsDir, "*.png", SearchOption.AllDirectories);
|
||||||
|
var pngMap = pngFiles
|
||||||
|
.GroupBy(Path.GetFileName)
|
||||||
|
.ToDictionary(g => g.Key!, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var card in cards)
|
foreach (var card in cards)
|
||||||
{
|
{
|
||||||
if (card.ImageFile == null) continue;
|
if (card.ImageFile == null) continue;
|
||||||
var src = Path.Combine(docsDir, card.ImageFile);
|
if (pngMap.TryGetValue(card.ImageFile, out var src))
|
||||||
var dst = Path.Combine(cardsDir, card.ImageFile);
|
{
|
||||||
if (File.Exists(src))
|
var dst = Path.Combine(cardsDir, card.ImageFile);
|
||||||
File.Copy(src, dst, true);
|
File.Copy(src, dst, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate C# source file
|
// Generate C# source file
|
||||||
@@ -119,8 +126,8 @@ writer.WriteLine("}");
|
|||||||
Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}");
|
Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}");
|
||||||
|
|
||||||
// ── Decks ──
|
// ── Decks ──
|
||||||
var decksDir = Path.Combine(docsDir, "Decks");
|
var decksDir = Path.Combine(docsDir, "Deck");
|
||||||
var deckFiles = Directory.Exists(decksDir) ? Directory.GetFiles(decksDir, "*.md") : [];
|
var deckFiles = Directory.Exists(decksDir) ? Directory.GetFiles(decksDir, "*.md", SearchOption.AllDirectories) : [];
|
||||||
var decks = new List<DeckData>();
|
var decks = new List<DeckData>();
|
||||||
|
|
||||||
foreach (var file in deckFiles)
|
foreach (var file in deckFiles)
|
||||||
@@ -135,6 +142,7 @@ foreach (var file in deckFiles)
|
|||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(file);
|
var name = Path.GetFileNameWithoutExtension(file);
|
||||||
var isVisible = yaml.GetValueOrDefault("isVisible") == "true";
|
var isVisible = yaml.GetValueOrDefault("isVisible") == "true";
|
||||||
|
var deckCode = yaml.GetValueOrDefault("deckCode");
|
||||||
|
|
||||||
var deck = new DeckData
|
var deck = new DeckData
|
||||||
{
|
{
|
||||||
@@ -142,9 +150,11 @@ foreach (var file in deckFiles)
|
|||||||
Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||||
Keycards = ParseList(yaml, "keycards").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(),
|
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(),
|
Factions = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
||||||
IsVisible = isVisible,
|
IsVisible = isVisible,
|
||||||
|
DeckCode = deckCode
|
||||||
};
|
};
|
||||||
|
|
||||||
decks.Add(deck);
|
decks.Add(deck);
|
||||||
@@ -178,6 +188,7 @@ for (var i = 0; i < decks.Count; i++)
|
|||||||
WriteStrProp(deckWriter, "Description", d.Description, 3);
|
WriteStrProp(deckWriter, "Description", d.Description, 3);
|
||||||
WriteListProp(deckWriter, "Factions", d.Factions, 3);
|
WriteListProp(deckWriter, "Factions", d.Factions, 3);
|
||||||
deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},");
|
deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},");
|
||||||
|
WriteStrProp(deckWriter, "DeckCode", d.DeckCode, 3);
|
||||||
var comma = i < decks.Count - 1 ? "," : "";
|
var comma = i < decks.Count - 1 ? "," : "";
|
||||||
deckWriter.WriteLine($" }}{comma}");
|
deckWriter.WriteLine($" }}{comma}");
|
||||||
}
|
}
|
||||||
@@ -267,6 +278,7 @@ static Dictionary<string, string> ParseYaml(string yaml)
|
|||||||
blockScalarLines.Add(trimmed);
|
blockScalarLines.Add(trimmed);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockScalarKey != null)
|
if (blockScalarKey != null)
|
||||||
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
||||||
inBlockScalar = false;
|
inBlockScalar = false;
|
||||||
@@ -285,8 +297,10 @@ static Dictionary<string, string> ParseYaml(string yaml)
|
|||||||
quoteKey = null;
|
quoteKey = null;
|
||||||
quoteLines.Clear();
|
quoteLines.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
||||||
quoteKey = null;
|
quoteKey = null;
|
||||||
quoteLines.Clear();
|
quoteLines.Clear();
|
||||||
@@ -347,24 +361,34 @@ static string UnescapeYaml(string s)
|
|||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
for (var i = 0; i < s.Length; i++)
|
for (var i = 0; i < s.Length; i++)
|
||||||
{
|
|
||||||
if (s[i] == '\\' && i + 1 < s.Length)
|
if (s[i] == '\\' && i + 1 < s.Length)
|
||||||
{
|
|
||||||
switch (s[i + 1])
|
switch (s[i + 1])
|
||||||
{
|
{
|
||||||
case 'r': sb.Append('\r'); i++; break;
|
case 'r':
|
||||||
case 'n': sb.Append('\n'); i++; break;
|
sb.Append('\r');
|
||||||
case 't': sb.Append('\t'); i++; break;
|
i++;
|
||||||
case '\\': sb.Append('\\'); i++; break;
|
break;
|
||||||
case '"': sb.Append('"'); 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;
|
default: sb.Append(s[i]); break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
sb.Append(s[i]);
|
sb.Append(s[i]);
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ var deployDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", ".
|
|||||||
var webDir = Path.GetFullPath(Path.Combine(deployDir, "..", "Web"));
|
var webDir = Path.GetFullPath(Path.Combine(deployDir, "..", "Web"));
|
||||||
var webProject = Path.Combine(webDir, "Web.csproj");
|
var webProject = Path.Combine(webDir, "Web.csproj");
|
||||||
var publishDir = Path.Combine(Path.GetTempPath(), "chrono-deploy", Guid.NewGuid().ToString());
|
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";
|
var deployEnv = Environment.GetEnvironmentVariable("Chrono_DeployEnv") ?? "preview";
|
||||||
|
|
||||||
// 1. Publish
|
// 1. Publish
|
||||||
@@ -20,7 +21,8 @@ if (!Directory.Exists(wwwroot))
|
|||||||
|
|
||||||
// 2. Deploy
|
// 2. Deploy
|
||||||
Console.WriteLine("Deploying to Azure Static Web Apps...");
|
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!");
|
Console.WriteLine("Deploy successful!");
|
||||||
return 0;
|
return 0;
|
||||||
@@ -29,5 +31,5 @@ static void Run(string fileName, string arguments)
|
|||||||
{
|
{
|
||||||
var process = Process.Start(new ProcessStartInfo(fileName, arguments) { UseShellExecute = false })!;
|
var process = Process.Start(new ProcessStartInfo(fileName, arguments) { UseShellExecute = false })!;
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
if (process.ExitCode != 0) { Environment.Exit(process.ExitCode); }
|
if (process.ExitCode != 0) Environment.Exit(process.ExitCode);
|
||||||
}
|
}
|
||||||
@@ -31,4 +31,16 @@ public class CardData
|
|||||||
public bool HasImmortalize => ImmortalizeTo is { Count: > 0 };
|
public bool HasImmortalize => ImmortalizeTo is { Count: > 0 };
|
||||||
public bool IsImmortalized => ImmortalizeFrom != null;
|
public bool IsImmortalized => ImmortalizeFrom != null;
|
||||||
public string ImagePath => $"cards/{ImageFile ?? "placeholder.png"}";
|
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)) ||
|
||||||
|
(Name.ToLowerInvariant().Contains("b.o.o.f.") && q.Equals("boof")) ||
|
||||||
|
(Name.ToLowerInvariant().Contains("boof") && q.Equals("b.o.o.f."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,19 @@ public class DeckData
|
|||||||
public List<string> Keycards { get; init; } = [];
|
public List<string> Keycards { get; init; } = [];
|
||||||
public List<string> Divers { get; init; } = [];
|
public List<string> Divers { get; init; } = [];
|
||||||
public string? Description { get; init; }
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
public string? DeckCode { get; init; }
|
||||||
|
|
||||||
public List<string> Factions { get; init; } = [];
|
public List<string> Factions { get; init; } = [];
|
||||||
public bool IsVisible { 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 Chrono.Model;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Server;
|
namespace Server;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Chrono.Model;
|
using Chrono.Model;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Server.Controllers;
|
namespace Server.Controllers;
|
||||||
|
|
||||||
@@ -21,10 +20,7 @@ public class NotesController : ControllerBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var note = await _context.CardNotes.FindAsync(cardName);
|
var note = await _context.CardNotes.FindAsync(cardName);
|
||||||
if (note == null)
|
if (note == null) return Ok(new CardNote { CardName = cardName, Note = "" });
|
||||||
{
|
|
||||||
return Ok(new CardNote { CardName = cardName, Note = "" });
|
|
||||||
}
|
|
||||||
return Ok(note);
|
return Ok(note);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -41,13 +37,9 @@ public class NotesController : ControllerBase
|
|||||||
{
|
{
|
||||||
var existing = await _context.CardNotes.FindAsync(note.CardName);
|
var existing = await _context.CardNotes.FindAsync(note.CardName);
|
||||||
if (existing == null)
|
if (existing == null)
|
||||||
{
|
|
||||||
_context.CardNotes.Add(note);
|
_context.CardNotes.Add(note);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
existing.Note = note.Note;
|
existing.Note = note.Note;
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ builder.Services.AddRazorPages();
|
|||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(connectionString))
|
if (!string.IsNullOrEmpty(connectionString)) options.UseNpgsql(connectionString);
|
||||||
{
|
|
||||||
options.UseNpgsql(connectionString);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -25,26 +22,20 @@ using (var scope = app.Services.CreateScope())
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
if (!string.IsNullOrEmpty(connectionString))
|
if (!string.IsNullOrEmpty(connectionString)) db.Database.Migrate();
|
||||||
{
|
|
||||||
db.Database.Migrate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
|
||||||
app.UseWebAssemblyDebugging();
|
app.UseWebAssemblyDebugging();
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
app.UseExceptionHandler("/Error");
|
app.UseExceptionHandler("/Error");
|
||||||
}
|
|
||||||
|
|
||||||
app.UseBlazorFrameworkFiles();
|
app.UseBlazorFrameworkFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|||||||
+21
-21
@@ -1,27 +1,27 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Model\Model.csproj" />
|
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||||
<ProjectReference Include="..\Web\Web.csproj" />
|
<ProjectReference Include="..\Web\Web.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9"/>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.Playwright.NUnit;
|
using Microsoft.Playwright.NUnit;
|
||||||
using Microsoft.Playwright;
|
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
@@ -7,45 +6,4 @@ namespace Tests;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class PlaywrightTests : PageTest
|
public class PlaywrightTests : PageTest
|
||||||
{
|
{
|
||||||
[Test]
|
|
||||||
public async Task CanWriteAndSaveNote()
|
|
||||||
{
|
|
||||||
// 1. Navigate to the cards gallery
|
|
||||||
await Page.GotoAsync("http://localhost:8080/cards");
|
|
||||||
|
|
||||||
// 2. Wait for cards to load - looking for at least one card-cell
|
|
||||||
await Expect(Page.Locator(".card-cell").First).ToBeVisibleAsync();
|
|
||||||
|
|
||||||
// 3. Find an Agent card and click it.
|
|
||||||
var agentCard = Page.Locator(".card-cell:has(.card-category-badge.agent)").First;
|
|
||||||
await agentCard.ClickAsync();
|
|
||||||
|
|
||||||
// 4. Wait for the detail view to show the note textarea
|
|
||||||
var noteInput = Page.Locator(".note-input");
|
|
||||||
await Expect(noteInput).ToBeVisibleAsync();
|
|
||||||
|
|
||||||
// 5. Type a unique note
|
|
||||||
string uniqueNote = "Test note " + Guid.NewGuid().ToString();
|
|
||||||
await noteInput.FillAsync(uniqueNote);
|
|
||||||
|
|
||||||
// 6. Blur to trigger save
|
|
||||||
await noteInput.BlurAsync();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Close the detail view by clicking the backdrop
|
|
||||||
await Page.Locator(".modal-backdrop").ClickAsync();
|
|
||||||
await Expect(noteInput).Not.ToBeVisibleAsync();
|
|
||||||
|
|
||||||
// 9. Re-open the same agent card
|
|
||||||
await agentCard.ClickAsync();
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ public class TelerikLicenseTests : PageTest
|
|||||||
await Expect(licenseWarning).Not.ToBeVisibleAsync();
|
await Expect(licenseWarning).Not.ToBeVisibleAsync();
|
||||||
|
|
||||||
// 4. Also verify that no "Trial" banner is visible
|
// 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();
|
await Expect(trialBanner).Not.ToBeVisibleAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0"/>
|
<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" Version="4.3.2"/>
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="4.7.0"/>
|
<PackageReference Include="NUnit.Analyzers" Version="4.7.0"/>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
|
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Model\Model.csproj" />
|
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
@namespace Chrono.Components
|
||||||
|
|
||||||
|
@if (Card != null)
|
||||||
|
{
|
||||||
|
<div class="modal-backdrop" @onclick="HandleBackdropClick"></div>
|
||||||
|
<div class="card-detail @(!HasImage ? "card-detail-noimg" : "")">
|
||||||
|
<button class="detail-close" @onclick="HandleClose"><i class="bi bi-x-lg"></i></button>
|
||||||
|
<div class="detail-layout @(!HasImage ? "layout-noimg" : "")">
|
||||||
|
@if (HasImage)
|
||||||
|
{
|
||||||
|
<div class="detail-image">
|
||||||
|
<img src="@Card.ImagePath" alt="@Card.Name"
|
||||||
|
onerror="this.style.display='none';this.parentElement.style.display='none'" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="detail-info">
|
||||||
|
<div class="detail-header">
|
||||||
|
<h2>@Card.Name</h2>
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span class="meta-badge category @Card.Category?.ToLowerInvariant()">@Card.Category</span>
|
||||||
|
@if (Card.Cost.HasValue)
|
||||||
|
{
|
||||||
|
<span class="meta-badge cost"><i class="bi bi-lightning-fill"></i> @Card.Cost</span>
|
||||||
|
}
|
||||||
|
@if (Card.Attack.HasValue)
|
||||||
|
{
|
||||||
|
<span class="meta-badge attack"><i class="bi bi-crosshair"></i> @Card.Attack</span>
|
||||||
|
}
|
||||||
|
@if (Card.Health.HasValue)
|
||||||
|
{
|
||||||
|
<span class="meta-badge health"><i class="bi bi-heart-fill"></i> @Card.Health</span>
|
||||||
|
}
|
||||||
|
@if (Card.Speed != null)
|
||||||
|
{
|
||||||
|
<span class="meta-badge speed"><i class="bi bi-wind"></i> @Card.Speed</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Card.Faction != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
|
||||||
|
<span class="field-value">@Card.Faction</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Card.Description != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field description">
|
||||||
|
<span class="field-label"><i class="bi bi-chat-quote-fill"></i></span>
|
||||||
|
<span class="field-value">@RenderDescription(Card.Description)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Card.Set != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-collection"></i> Set</span>
|
||||||
|
<span class="field-value">@Card.Set</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Card.Archetypes is { Count: > 0 })
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-layers-fill"></i> Archetypes</span>
|
||||||
|
<span class="field-value">@string.Join(", ", Card.Archetypes)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Card.ImmortalizeWhen != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-star-fill"></i> Immortalize When</span>
|
||||||
|
<span class="field-value">@Card.ImmortalizeWhen</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Card.HasImmortalize)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-arrow-right-circle-fill"></i> Immortalizes To</span>
|
||||||
|
<span class="field-value">
|
||||||
|
@foreach (var name in Card.ImmortalizeTo!)
|
||||||
|
{
|
||||||
|
var targetName = name;
|
||||||
|
var target = LookupCard(targetName);
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
<button class="inline-card-btn" @onclick="() => Navigate(target)">@targetName</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@targetName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Card.ImmortalizeFrom != null)
|
||||||
|
{
|
||||||
|
<div class="detail-field">
|
||||||
|
<span class="field-label"><i class="bi bi-arrow-left-circle-fill"></i> Immortalizes From</span>
|
||||||
|
<span class="field-value">
|
||||||
|
@{
|
||||||
|
var fromCard = LookupCard(Card.ImmortalizeFrom);
|
||||||
|
if (fromCard != null)
|
||||||
|
{
|
||||||
|
<button class="inline-card-btn" @onclick="() => Navigate(fromCard)">@Card.ImmortalizeFrom</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@Card.ImmortalizeFrom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (ShowNotes && Card.IsAgent)
|
||||||
|
{
|
||||||
|
<div class="detail-field note">
|
||||||
|
<span class="field-label"><i class="bi bi-pencil-fill"></i> Personal Note</span>
|
||||||
|
<textarea class="form-control note-input" @bind="currentNote" @onblur="SaveNote"
|
||||||
|
placeholder="Add a private note about this agent..."></textarea>
|
||||||
|
@if (isSaving)
|
||||||
|
{
|
||||||
|
<span class="saving-indicator">Saving...</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public CardData? Card { get; set; }
|
||||||
|
[Parameter] public EventCallback OnClose { get; set; }
|
||||||
|
[Parameter] public EventCallback<CardData> OnNavigate { get; set; }
|
||||||
|
[Parameter] public bool ShowNotes { get; set; }
|
||||||
|
|
||||||
|
[Inject] private HttpClient Http { get; set; } = default!;
|
||||||
|
|
||||||
|
private bool HasImage => Card?.ImageFile is { Length: > 0 } && Card.ImageFile != "placeholder.png";
|
||||||
|
|
||||||
|
private string currentNote = "";
|
||||||
|
private bool isSaving;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (Card != null && Card.IsAgent)
|
||||||
|
{
|
||||||
|
currentNote = "";
|
||||||
|
_ = LoadNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadNote()
|
||||||
|
{
|
||||||
|
if (Card == null || !Card.IsAgent) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var note = await Http.GetFromJsonAsync<CardNote>($"api/notes/{Uri.EscapeDataString(Card.Name)}");
|
||||||
|
currentNote = note?.Note ?? "";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
currentNote = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveNote()
|
||||||
|
{
|
||||||
|
if (Card == null || !Card.IsAgent) return;
|
||||||
|
|
||||||
|
isSaving = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = Card.Name, Note = currentNote });
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleClose()
|
||||||
|
{
|
||||||
|
_ = OnClose.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleBackdropClick()
|
||||||
|
{
|
||||||
|
_ = OnClose.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Navigate(CardData card)
|
||||||
|
{
|
||||||
|
_ = OnNavigate.InvokeAsync(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CardData? LookupCard(string cardName)
|
||||||
|
{
|
||||||
|
return CardDatabase.Cards.FirstOrDefault(c =>
|
||||||
|
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderDescription(string description) => builder =>
|
||||||
|
{
|
||||||
|
var cardNames = CardDatabase.Cards
|
||||||
|
.Select(c => c.Name)
|
||||||
|
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||||
|
.OrderByDescending(n => n.Length)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int seq = 0;
|
||||||
|
var remaining = description;
|
||||||
|
|
||||||
|
while (!string.IsNullOrEmpty(remaining))
|
||||||
|
{
|
||||||
|
int bestIdx = -1;
|
||||||
|
string? bestName = null;
|
||||||
|
|
||||||
|
foreach (var name in cardNames)
|
||||||
|
{
|
||||||
|
int idx = remaining.IndexOf(name, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (idx != -1 && (bestIdx == -1 || idx < bestIdx))
|
||||||
|
{
|
||||||
|
bestIdx = idx;
|
||||||
|
bestName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestIdx != -1 && bestName != null)
|
||||||
|
{
|
||||||
|
if (bestIdx > 0)
|
||||||
|
builder.AddContent(seq++, remaining[..bestIdx]);
|
||||||
|
|
||||||
|
var card = LookupCard(bestName);
|
||||||
|
if (card != null)
|
||||||
|
{
|
||||||
|
builder.OpenElement(seq++, "button");
|
||||||
|
builder.AddAttribute(seq++, "class", "inline-card-btn");
|
||||||
|
builder.AddAttribute(seq++, "onclick",
|
||||||
|
EventCallback.Factory.Create(this, () => Navigate(card)));
|
||||||
|
builder.AddContent(seq++, bestName);
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AddContent(seq++, bestName);
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining = remaining[(bestIdx + bestName.Length)..];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AddContent(seq++, remaining);
|
||||||
|
remaining = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1040;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
animation: fade-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1050;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 720px;
|
||||||
|
width: 92vw;
|
||||||
|
max-height: 88vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
color: var(--text-primary);
|
||||||
|
animation: detail-enter 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes detail-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.92);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
flex: 0 0 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── No-image layout ── */
|
||||||
|
.card-detail-noimg {
|
||||||
|
max-width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-noimg {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
border-radius: 100px;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.category.agent,
|
||||||
|
.meta-badge.category.immortalized {
|
||||||
|
background: rgba(79, 195, 247, 0.15);
|
||||||
|
color: #4fc3f7;
|
||||||
|
border-color: rgba(79, 195, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.category.spell {
|
||||||
|
background: rgba(206, 147, 216, 0.15);
|
||||||
|
color: #ce93d8;
|
||||||
|
border-color: rgba(206, 147, 216, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.cost {
|
||||||
|
background: rgba(255, 215, 0, 0.12);
|
||||||
|
color: var(--gold);
|
||||||
|
border-color: rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.attack {
|
||||||
|
background: rgba(239, 83, 80, 0.15);
|
||||||
|
color: #ef5350;
|
||||||
|
border-color: rgba(239, 83, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.health {
|
||||||
|
background: rgba(102, 187, 106, 0.15);
|
||||||
|
color: #66bb6a;
|
||||||
|
border-color: rgba(102, 187, 106, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge.speed {
|
||||||
|
background: rgba(79, 195, 247, 0.12);
|
||||||
|
color: #4fc3f7;
|
||||||
|
border-color: rgba(79, 195, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field.description {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 7.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field.description .field-label {
|
||||||
|
min-width: auto;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-field.description .field-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-color: rgba(108, 99, 255, 0.3);
|
||||||
|
transition: color var(--transition), text-decoration-color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-btn:hover {
|
||||||
|
color: #7b73ff;
|
||||||
|
text-decoration-color: #7b73ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
.detail-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail,
|
||||||
|
.card-detail-noimg {
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail-noimg {
|
||||||
|
max-width: 92vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
+4092
-4072
File diff suppressed because it is too large
Load Diff
@@ -12,38 +12,63 @@ public static class DeckDatabase
|
|||||||
Name = "Big Energy",
|
Name = "Big Energy",
|
||||||
Cards = [
|
Cards = [
|
||||||
"Brilliant Martyr",
|
"Brilliant Martyr",
|
||||||
|
"Brilliant Martyr",
|
||||||
|
"Brilliant Martyr",
|
||||||
|
"Kinetic Absorber",
|
||||||
|
"Kinetic Absorber",
|
||||||
"Kinetic Absorber",
|
"Kinetic Absorber",
|
||||||
"Hidden Locus",
|
"Hidden Locus",
|
||||||
|
"Hidden Locus",
|
||||||
|
"Hidden Locus",
|
||||||
"Suncursed Conduit",
|
"Suncursed Conduit",
|
||||||
"Swashbuckling Diehard",
|
"Suncursed Conduit",
|
||||||
"Debris Collector",
|
"Suncursed Conduit",
|
||||||
"Paradox Flow",
|
"Paradox Capacitor",
|
||||||
"Starfueled Medics",
|
"Paradox Capacitor",
|
||||||
|
"Lumbering Starseeker",
|
||||||
"Lumbering Starseeker",
|
"Lumbering Starseeker",
|
||||||
"Novathermal Mining",
|
"Novathermal Mining",
|
||||||
|
"Novathermal Mining",
|
||||||
|
"Novathermal Mining",
|
||||||
"Radiant Channeling",
|
"Radiant Channeling",
|
||||||
|
"Radiant Channeling",
|
||||||
|
"Radiant Channeling",
|
||||||
|
"Supernova",
|
||||||
|
"Supernova",
|
||||||
"Supernova",
|
"Supernova",
|
||||||
"Gunnery Captain",
|
"Gunnery Captain",
|
||||||
"Lightsteel Colossus",
|
"Lightsteel Colossus",
|
||||||
|
"Lightsteel Colossus",
|
||||||
|
"Devourer Spawn",
|
||||||
"Devourer Spawn",
|
"Devourer Spawn",
|
||||||
"Army of the Sun",
|
"Army of the Sun",
|
||||||
|
"Curb the Anomalies",
|
||||||
|
"Curb the Anomalies",
|
||||||
|
"Curb the Anomalies",
|
||||||
|
"Bearer of the Broth",
|
||||||
|
"Bearer of the Broth",
|
||||||
|
"Bearer of the Broth",
|
||||||
|
"Blessed Soup",
|
||||||
|
"Blessed Soup",
|
||||||
|
"Blessed Soup",
|
||||||
],
|
],
|
||||||
Keycards = [
|
Keycards = [
|
||||||
"Kinetic Absorber",
|
"Kinetic Absorber",
|
||||||
"Hidden Locus",
|
|
||||||
"Debris Collector",
|
|
||||||
"Lumbering Starseeker",
|
"Lumbering Starseeker",
|
||||||
"Lightsteel Colossus",
|
"Lightsteel Colossus",
|
||||||
|
"Curb the Anomalies",
|
||||||
],
|
],
|
||||||
Divers = [
|
Divers = [
|
||||||
"Peaceful Synthesizer",
|
"Peaceful Synthesizer",
|
||||||
"Limit Breaker",
|
"Nonlethal Special Forces",
|
||||||
],
|
],
|
||||||
Description = "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 Kinetic Absorber, 5/6 Lumbering Starseeker, and 8 for a 9/14 Lightsteel Colossus. Debris Collector is also quite powerful and will Immortalize into a Living Comet 10/10 with Overpower pretty reliably, but it's also your only stat Mute target, so it might be a bit trash if that's popular in the meta.\nYou got Devourer Spawn to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta.\nYou need to Overflow your mana once in a while, or some of your Supernova removal will be useless. Be pass heavy.\nYou're going to win on very low health if you win. Such is life.\nNo card draw, so Army of the Sun is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.\nStarfueled Medics and Swashbuckling Diehard are just there to take up space and be mid-range drops. And technically, they can be win conditions, I suppose.\nAttack aggressively with Brilliant Martyr. You really want Star Siphon to ramp.\nAs divers, Peaceful Synthesizer is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. And Limit Breaker is an early-game growing threat that will demand an answer from the enemy.",
|
Description = "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 Kinetic Absorber, 5/6 Lumbering Starseeker, and 8 for a 9/14 Lightsteel Colossus.\nYou got Devourer Spawn to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta.\nYou need to Overflow your mana once in a while, or some of your Supernova removal will be useless. Be pass heavy.\nYou're going to win on very low health if you win. Such is life.\nNo strong card draw, so Army of the Sun is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.\nAttack aggressively with Brilliant Martyr. You really want Star Siphon to ramp.\nAs divers, Peaceful Synthesizer is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. Nonlethal Special Forces is to enable Phasetide for AOE Mute cards given we don't care about stat buffs. Also as it stands, it always seems to punch a above it's weight on the board given the Disarm effect.\nSo ya, Curb the Anomalies can weaken the enemy board, by shutting off all there better stats, activates, and synergies. Hopefully destroy the entire enemy board in combat.\nAnd I suppose Bearer of the Broth. Since the deck has so much raw stats, you will probably find somewhere to heal. Like healing Kinetic Absorber so it can keep killing after it Immortalizes.",
|
||||||
Factions = [
|
Factions = [
|
||||||
"Sungrace",
|
"Sungrace",
|
||||||
|
"Phasetide",
|
||||||
],
|
],
|
||||||
IsVisible = true,
|
IsVisible = true,
|
||||||
|
DeckCode = "AUHUE2LHEBCW4ZLSM54SAQ3POB4QWY3PNZZXI4TVMN2GKZBTAADACASPCICESCQCAUFDCAYGAMCAGAQDDYBQCAYBAOTQCAYQAMDQG",
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<div class="top-row ps-3 navbar navbar-dark">
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
<div class="container-fluid">
|
<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">
|
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
Groupable="true"
|
Groupable="true"
|
||||||
class="agents-grid">
|
class="agents-grid">
|
||||||
<GridColumns>
|
<GridColumns>
|
||||||
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
||||||
<Template>
|
<Template>
|
||||||
@if (((CardData)context).IsImmortalized)
|
@if (((CardData)context).IsImmortalized)
|
||||||
{
|
{
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<Template>
|
<Template>
|
||||||
@(((CardData)context).StatEfficiency)
|
@(((CardData)context).StatEfficiency)
|
||||||
</Template>
|
</Template>
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn Field="@nameof(CardData.Faction)" Title="Faction" Width="140px"/>
|
<GridColumn Field="@nameof(CardData.Faction)" Title="Faction" Width="140px"/>
|
||||||
<GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/>
|
<GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/>
|
||||||
<GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/>
|
<GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/>
|
||||||
|
|||||||
+85
-153
@@ -13,15 +13,21 @@
|
|||||||
<button class="tab @(categoryFilter == "" ? "active" : "")" @onclick='@(() => SetCategory(""))'>
|
<button class="tab @(categoryFilter == "" ? "active" : "")" @onclick='@(() => SetCategory(""))'>
|
||||||
<i class="bi bi-grid-3x3-gap-fill"></i> All
|
<i class="bi bi-grid-3x3-gap-fill"></i> All
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="tab agent @(categoryFilter == "Agent" ? "active" : "")" @onclick='@(() => SetCategory("Agent"))'>
|
<button class="tab agent @(categoryFilter == "Agent" ? "active" : "")" @onclick='@(() => SetCategory("Agent"))'>
|
||||||
<i class="bi bi-person-fill"></i> Agents
|
<i class="bi bi-person-fill"></i> Agents
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tab agent @(categoryFilter == "Immortalized" ? "active" : "")" @onclick='@(() => SetCategory("Immortalized"))'>
|
||||||
|
<i class="bi bi-person-fill"></i> Immortalized
|
||||||
|
</button>
|
||||||
|
<!-- //Todo make agent being linked to immortal for side by side sorting
|
||||||
|
<button class="tab agent @(agentLink ? "active" : "")" @onclick='@(() => ToggleAgentLink())'>
|
||||||
|
<i class="bi bi-person-fill"></i> Link
|
||||||
|
</button>
|
||||||
|
-->
|
||||||
<button class="tab spell @(categoryFilter == "Spell" ? "active" : "")" @onclick='@(() => SetCategory("Spell"))'>
|
<button class="tab spell @(categoryFilter == "Spell" ? "active" : "")" @onclick='@(() => SetCategory("Spell"))'>
|
||||||
<i class="bi bi-wand"></i> Spells
|
<i class="bi bi-wand"></i> Spells
|
||||||
</button>
|
</button>
|
||||||
<button class="tab token @(categoryFilter == "Token" ? "active" : "")" @onclick='@(() => SetCategory("Token"))'>
|
|
||||||
<i class="bi bi-coin"></i> Tokens
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
@@ -43,11 +49,28 @@
|
|||||||
</select>
|
</select>
|
||||||
<select @bind="costFilter" class="form-select filter-select">
|
<select @bind="costFilter" class="form-select filter-select">
|
||||||
<option value="">All Costs</option>
|
<option value="">All Costs</option>
|
||||||
@for (var i = 0; i <= 12; i++)
|
@for (var i = 0; i <= 13; i++)
|
||||||
{
|
{
|
||||||
<option value="@i">@i</option>
|
<option value="@i">@i</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="sort-group d-flex gap-1">
|
||||||
|
<select @bind="sortBy" class="form-select filter-select sort-select">
|
||||||
|
<option value="Cost">Sort by Cost</option>
|
||||||
|
<option value="Name">Sort by Name</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (HasActiveFilters)
|
@if (HasActiveFilters)
|
||||||
@@ -89,9 +112,9 @@
|
|||||||
|
|
||||||
@if (filteredCards.Any())
|
@if (filteredCards.Any())
|
||||||
{
|
{
|
||||||
<div class="card-grid">
|
<div class="card-grid @(showDetailedView ? "detailed-view" : "")">
|
||||||
@{ var idx = 0; }
|
@{ var idx = 0; }
|
||||||
@foreach (var card in filteredCards)
|
@foreach (var card in filteredCards.Where(a=>a.Category is "Agent" or "Spell" or "Immortalized"))
|
||||||
{
|
{
|
||||||
<div class="card-cell @(selectedCard == card ? "selected" : "")"
|
<div class="card-cell @(selectedCard == card ? "selected" : "")"
|
||||||
style="--i: @idx"
|
style="--i: @idx"
|
||||||
@@ -100,20 +123,31 @@
|
|||||||
<div class="card-shimmer"></div>
|
<div class="card-shimmer"></div>
|
||||||
<img src="@card.ImagePath" alt="@card.Name" loading="lazy"
|
<img src="@card.ImagePath" alt="@card.Name" loading="lazy"
|
||||||
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22280%22><rect fill=%22%23222244%22 width=%22200%22 height=%22280%22/><text fill=%22%23686888%22 font-size=%2214%22 x=%22100%22 y=%22140%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>No Image</text></svg>'"/>
|
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22280%22><rect fill=%22%23222244%22 width=%22200%22 height=%22280%22/><text fill=%22%23686888%22 font-size=%2214%22 x=%22100%22 y=%22140%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>No Image</text></svg>'"/>
|
||||||
@if (card.Cost.HasValue)
|
@if (card.IsImmortalized)
|
||||||
{
|
|
||||||
<div class="card-cost-badge">@card.Cost</div>
|
|
||||||
}
|
|
||||||
@if (card.HasImmortalize)
|
|
||||||
{
|
{
|
||||||
<div class="card-immortalize-badge" title="Immortalizes"><i class="bi bi-star-fill"></i>
|
<div class="card-immortalize-badge" title="Immortalizes"><i class="bi bi-star-fill"></i>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-label">
|
@if (showDetailedView)
|
||||||
<div class="card-name">@card.Name</div>
|
{
|
||||||
<div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div>
|
<div class="card-label">
|
||||||
</div>
|
<div class="card-name">@card.Name</div>
|
||||||
|
<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>
|
</div>
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
@@ -129,105 +163,11 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@if (selectedCard != null)
|
@if (selectedCard != null)
|
||||||
{
|
{
|
||||||
<div class="modal-backdrop" @onclick="CloseDetail"></div>
|
<CardDialog Card="selectedCard" OnClose="CloseDetail" OnNavigate="SelectCard" />
|
||||||
<div class="card-detail">
|
|
||||||
<button class="detail-close" @onclick="CloseDetail"><i class="bi bi-x-lg"></i></button>
|
|
||||||
<div class="detail-layout">
|
|
||||||
<div class="detail-image">
|
|
||||||
<img src="@selectedCard.ImagePath" alt="@selectedCard.Name"/>
|
|
||||||
</div>
|
|
||||||
<div class="detail-info">
|
|
||||||
<div class="detail-header">
|
|
||||||
<h2>@selectedCard.Name</h2>
|
|
||||||
<div class="detail-meta">
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Attack.HasValue)
|
|
||||||
{
|
|
||||||
<span class="meta-badge attack"><i class="bi bi-crosshair"></i> @selectedCard.Attack</span>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Health.HasValue)
|
|
||||||
{
|
|
||||||
<span class="meta-badge health"><i class="bi bi-heart-fill"></i> @selectedCard.Health</span>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Speed != null)
|
|
||||||
{
|
|
||||||
<span class="meta-badge speed"><i class="bi bi-wind"></i> @selectedCard.Speed</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (selectedCard.Faction != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
|
|
||||||
<span class="field-value">@selectedCard.Faction</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Description != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field description">
|
|
||||||
<span class="field-label"><i class="bi bi-chat-quote-fill"></i></span>
|
|
||||||
<span class="field-value">@selectedCard.Description</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Set != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-collection"></i> Set</span>
|
|
||||||
<span class="field-value">@selectedCard.Set</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Archetypes is { Count: > 0 })
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-layers-fill"></i> Archetypes</span>
|
|
||||||
<span class="field-value">@string.Join(", ", selectedCard.Archetypes)</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.ImmortalizeWhen != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-star-fill"></i> Immortalize When</span>
|
|
||||||
<span class="field-value">@selectedCard.ImmortalizeWhen</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.HasImmortalize)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-arrow-right-circle-fill"></i> Immortalizes To</span>
|
|
||||||
<span class="field-value">@string.Join(", ", selectedCard.ImmortalizeTo!)</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.ImmortalizeFrom != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-arrow-left-circle-fill"></i> Immortalizes From</span>
|
|
||||||
<span class="field-value">@selectedCard.ImmortalizeFrom</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (selectedCard.IsAgent)
|
|
||||||
{
|
|
||||||
<div class="detail-field note">
|
|
||||||
<span class="field-label"><i class="bi bi-pencil-fill"></i> Personal Note</span>
|
|
||||||
<textarea class="form-control note-input" @bind="currentNote" @onblur="SaveNote"
|
|
||||||
placeholder="Add a private note about this agent..."></textarea>
|
|
||||||
@if (isSaving)
|
|
||||||
{
|
|
||||||
<span class="saving-indicator">Saving...</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@@ -235,14 +175,17 @@
|
|||||||
private IEnumerable<CardData> filteredCards => ApplyFilters();
|
private IEnumerable<CardData> filteredCards => ApplyFilters();
|
||||||
private string search = "";
|
private string search = "";
|
||||||
private string categoryFilter = "";
|
private string categoryFilter = "";
|
||||||
|
private string immortalFilter = "";
|
||||||
|
private bool agentLink = true;
|
||||||
private string factionFilter = "";
|
private string factionFilter = "";
|
||||||
private string costFilter = "";
|
private string costFilter = "";
|
||||||
|
private string sortBy = "Cost";
|
||||||
|
private bool sortDescending;
|
||||||
|
private bool showDetailedView;
|
||||||
private CardData? selectedCard;
|
private CardData? selectedCard;
|
||||||
private List<string> factions = [];
|
private List<string> factions = [];
|
||||||
private string currentNote = "";
|
|
||||||
private bool isSaving = false;
|
|
||||||
|
|
||||||
private bool HasActiveFilters => categoryFilter != "" || factionFilter != "" || costFilter != "";
|
private bool HasActiveFilters => categoryFilter != "" || factionFilter != "" || costFilter != "" || immortalFilter != "";
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
@@ -257,14 +200,31 @@
|
|||||||
|
|
||||||
private IEnumerable<CardData> ApplyFilters()
|
private IEnumerable<CardData> ApplyFilters()
|
||||||
{
|
{
|
||||||
var q = search?.Trim().ToLowerInvariant() ?? "";
|
var filtered = allCards.Where(c =>
|
||||||
return allCards.Where(c =>
|
c.MatchesSearch(search) &&
|
||||||
(q.Length == 0 || c.Name.ToLowerInvariant().Contains(q) ||
|
|
||||||
(c.Description?.ToLowerInvariant().Contains(q) ?? false)) &&
|
|
||||||
(categoryFilter == "" || c.Category == categoryFilter) &&
|
(categoryFilter == "" || c.Category == categoryFilter) &&
|
||||||
(factionFilter == "" || c.Faction == factionFilter) &&
|
(factionFilter == "" || c.Faction == factionFilter) &&
|
||||||
(costFilter == "" || c.Cost?.ToString() == costFilter)
|
(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 SetImmortal(string imm)
|
||||||
|
{
|
||||||
|
immortalFilter = immortalFilter == imm ? "" : imm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearImmortalFilter()
|
||||||
|
{
|
||||||
|
immortalFilter = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetCategory(string cat)
|
private void SetCategory(string cat)
|
||||||
@@ -282,6 +242,11 @@
|
|||||||
factionFilter = "";
|
factionFilter = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleAgentLink()
|
||||||
|
{
|
||||||
|
agentLink = !agentLink;
|
||||||
|
}
|
||||||
|
|
||||||
private void ClearCostFilter()
|
private void ClearCostFilter()
|
||||||
{
|
{
|
||||||
costFilter = "";
|
costFilter = "";
|
||||||
@@ -292,41 +257,9 @@
|
|||||||
search = "";
|
search = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SelectCard(CardData card)
|
private void SelectCard(CardData card)
|
||||||
{
|
{
|
||||||
selectedCard = card;
|
selectedCard = card;
|
||||||
if (card.IsAgent)
|
|
||||||
{
|
|
||||||
currentNote = "";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var note = await Http.GetFromJsonAsync<CardNote>($"api/notes/{Uri.EscapeDataString(card.Name)}");
|
|
||||||
currentNote = note?.Note ?? "";
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
currentNote = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveNote()
|
|
||||||
{
|
|
||||||
if (selectedCard == null || !selectedCard.IsAgent) return;
|
|
||||||
|
|
||||||
isSaving = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = selectedCard.Name, Note = currentNote });
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Error handling
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
isSaving = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseDetail()
|
private void CloseDetail()
|
||||||
@@ -341,5 +274,4 @@
|
|||||||
factionFilter = "";
|
factionFilter = "";
|
||||||
costFilter = "";
|
costFilter = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-235
@@ -210,6 +210,101 @@
|
|||||||
gap: 1rem;
|
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 ── */
|
||||||
.card-cell {
|
.card-cell {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -397,225 +492,6 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Detail Modal ── */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1040;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
animation: fade-in 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 1050;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 0;
|
|
||||||
max-width: 720px;
|
|
||||||
width: 92vw;
|
|
||||||
max-height: 88vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
|
||||||
color: var(--text-primary);
|
|
||||||
animation: detail-enter 0.25s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes detail-enter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(-50%, -50%) scale(0.92);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(-50%, -50%) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.75rem;
|
|
||||||
right: 0.75rem;
|
|
||||||
z-index: 1;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
color: var(--text-primary);
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: all var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-close:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-image {
|
|
||||||
flex: 0 0 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-image img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header h2 {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.25rem 0.6rem;
|
|
||||||
border-radius: 100px;
|
|
||||||
background: var(--bg-hover);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category {
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category.agent {
|
|
||||||
background: rgba(79, 195, 247, 0.15);
|
|
||||||
color: #4fc3f7;
|
|
||||||
border-color: rgba(79, 195, 247, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category.spell {
|
|
||||||
background: rgba(206, 147, 216, 0.15);
|
|
||||||
color: #ce93d8;
|
|
||||||
border-color: rgba(206, 147, 216, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category.token {
|
|
||||||
background: rgba(255, 213, 79, 0.15);
|
|
||||||
color: #ffd54f;
|
|
||||||
border-color: rgba(255, 213, 79, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.cost {
|
|
||||||
background: rgba(255, 215, 0, 0.12);
|
|
||||||
color: var(--gold);
|
|
||||||
border-color: rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.attack {
|
|
||||||
background: rgba(239, 83, 80, 0.15);
|
|
||||||
color: #ef5350;
|
|
||||||
border-color: rgba(239, 83, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.health {
|
|
||||||
background: rgba(102, 187, 106, 0.15);
|
|
||||||
color: #66bb6a;
|
|
||||||
border-color: rgba(102, 187, 106, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.speed {
|
|
||||||
background: rgba(79, 195, 247, 0.12);
|
|
||||||
color: #4fc3f7;
|
|
||||||
border-color: rgba(79, 195, 247, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Detail Fields ── */
|
|
||||||
.detail-field {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field.description {
|
|
||||||
background: var(--bg-elevated);
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border-left: 3px solid var(--accent);
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 7.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field.description .field-label {
|
|
||||||
min-width: auto;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-value {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field.description .field-value {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Scrollbar for modal ── */
|
|
||||||
.card-detail::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.gallery-container {
|
.gallery-container {
|
||||||
@@ -632,23 +508,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-grid {
|
.card-grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-layout {
|
.card-grid.detailed-view {
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr;
|
||||||
padding: 1rem;
|
max-width: 500px;
|
||||||
}
|
|
||||||
|
|
||||||
.detail-image {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
max-width: 180px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail {
|
|
||||||
max-height: 90vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@@ -658,9 +527,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.card-grid {
|
.card-grid, .card-grid.detailed-view {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-tabs {
|
.category-tabs {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<article class="deck-article">
|
<article class="deck-article">
|
||||||
<header class="deck-header">
|
<header class="deck-header">
|
||||||
<h1>@deck.Name</h1>
|
<h1>@deck.Name</h1>
|
||||||
|
<b>Deck Code:</b> <pre>@deck.DeckCode</pre>
|
||||||
@if (deck.Factions.Count > 0)
|
@if (deck.Factions.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="deck-factions">
|
<div class="deck-factions">
|
||||||
@@ -26,7 +27,16 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
@if (deck.Keycards.Count > 0)
|
@if (deck.Keycards.Count > 0)
|
||||||
@@ -40,9 +50,9 @@
|
|||||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||||
<div class="mini-card">
|
<div class="mini-card">
|
||||||
<div class="mini-card-img">
|
<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>
|
||||||
<div class="mini-card-name">@cardName</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -61,9 +71,9 @@
|
|||||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||||
<div class="mini-card">
|
<div class="mini-card">
|
||||||
<div class="mini-card-img">
|
<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>
|
||||||
<div class="mini-card-name">@cardName</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -74,15 +84,23 @@
|
|||||||
<section class="deck-section">
|
<section class="deck-section">
|
||||||
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
|
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
|
||||||
<div class="deck-card-row">
|
<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);
|
var card = LookupCard(cardName);
|
||||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
<button class="mini-card-btn @(count > 1 ? "is-stacked" : "")"
|
||||||
<div class="mini-card">
|
@onclick="() => SelectCard(card)">
|
||||||
<div class="mini-card-img">
|
<div class="mini-card-stack">
|
||||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
@for (var i = 0; i < (count > 1 ? Math.Min(count, 3) : 1); i++)
|
||||||
</div>
|
{
|
||||||
<div class="mini-card-name">@cardName</div>
|
<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>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -94,10 +112,7 @@
|
|||||||
<section class="deck-section description">
|
<section class="deck-section description">
|
||||||
<h2><i class="bi bi-chat-quote-fill"></i> Description</h2>
|
<h2><i class="bi bi-chat-quote-fill"></i> Description</h2>
|
||||||
<div class="deck-description">
|
<div class="deck-description">
|
||||||
@foreach (var paragraph in deck.Description.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
@RenderDescription(deck.Description)
|
||||||
{
|
|
||||||
<p>@paragraph</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@@ -107,94 +122,11 @@
|
|||||||
|
|
||||||
@if (selectedCard != null)
|
@if (selectedCard != null)
|
||||||
{
|
{
|
||||||
<div class="modal-backdrop" @onclick="CloseDetail"></div>
|
<CardDialog Card="selectedCard" OnClose="CloseDetail" OnNavigate="SelectCard" />
|
||||||
<div class="card-detail">
|
|
||||||
<button class="detail-close" @onclick="CloseDetail"><i class="bi bi-x-lg"></i></button>
|
|
||||||
<div class="detail-layout">
|
|
||||||
<div class="detail-image">
|
|
||||||
<img src="@selectedCard.ImagePath" alt="@selectedCard.Name"/>
|
|
||||||
</div>
|
|
||||||
<div class="detail-info">
|
|
||||||
<div class="detail-header">
|
|
||||||
<h2>@selectedCard.Name</h2>
|
|
||||||
<div class="detail-meta">
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Attack.HasValue)
|
|
||||||
{
|
|
||||||
<span class="meta-badge attack"><i class="bi bi-crosshair"></i> @selectedCard.Attack</span>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Health.HasValue)
|
|
||||||
{
|
|
||||||
<span class="meta-badge health"><i class="bi bi-heart-fill"></i> @selectedCard.Health</span>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Speed != null)
|
|
||||||
{
|
|
||||||
<span class="meta-badge speed"><i class="bi bi-wind"></i> @selectedCard.Speed</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (selectedCard.Faction != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
|
|
||||||
<span class="field-value">@selectedCard.Faction</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Description != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field description">
|
|
||||||
<span class="field-label"><i class="bi bi-chat-quote-fill"></i></span>
|
|
||||||
<span class="field-value">@selectedCard.Description</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Set != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-collection"></i> Set</span>
|
|
||||||
<span class="field-value">@selectedCard.Set</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.Archetypes is { Count: > 0 })
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-layers-fill"></i> Archetypes</span>
|
|
||||||
<span class="field-value">@string.Join(", ", selectedCard.Archetypes)</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.ImmortalizeWhen != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-star-fill"></i> Immortalize When</span>
|
|
||||||
<span class="field-value">@selectedCard.ImmortalizeWhen</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.HasImmortalize)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-arrow-right-circle-fill"></i> Immortalizes To</span>
|
|
||||||
<span class="field-value">@string.Join(", ", selectedCard.ImmortalizeTo!)</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (selectedCard.ImmortalizeFrom != null)
|
|
||||||
{
|
|
||||||
<div class="detail-field">
|
|
||||||
<span class="field-label"><i class="bi bi-arrow-left-circle-fill"></i> Immortalizes From</span>
|
|
||||||
<span class="field-value">@selectedCard.ImmortalizeFrom</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter] public string Name { get; set; } = "";
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
private DeckData? deck;
|
private DeckData? deck;
|
||||||
private CardData? selectedCard;
|
private CardData? selectedCard;
|
||||||
@@ -220,4 +152,66 @@
|
|||||||
{
|
{
|
||||||
selectedCard = null;
|
selectedCard = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderDescription(string description)
|
||||||
|
{
|
||||||
|
return builder =>
|
||||||
|
{
|
||||||
|
var cardNames = CardDatabase.Cards
|
||||||
|
.Select(c => c.Name)
|
||||||
|
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||||
|
.OrderByDescending(n => n.Length)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int sequence = 0;
|
||||||
|
var paragraphs = description.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
foreach (var paragraph in paragraphs)
|
||||||
|
{
|
||||||
|
builder.OpenElement(sequence++, "p");
|
||||||
|
|
||||||
|
var currentText = paragraph;
|
||||||
|
while (!string.IsNullOrEmpty(currentText))
|
||||||
|
{
|
||||||
|
int bestMatchIndex = -1;
|
||||||
|
string? bestMatchName = null;
|
||||||
|
|
||||||
|
foreach (var name in cardNames)
|
||||||
|
{
|
||||||
|
int index = currentText.IndexOf(name, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (index != -1 && (bestMatchIndex == -1 || index < bestMatchIndex))
|
||||||
|
{
|
||||||
|
bestMatchIndex = index;
|
||||||
|
bestMatchName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatchIndex != -1 && bestMatchName != null)
|
||||||
|
{
|
||||||
|
if (bestMatchIndex > 0)
|
||||||
|
{
|
||||||
|
builder.AddContent(sequence++, currentText.Substring(0, bestMatchIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
var card = LookupCard(bestMatchName);
|
||||||
|
builder.OpenElement(sequence++, "button");
|
||||||
|
builder.AddAttribute(sequence++, "class", "inline-card-btn");
|
||||||
|
builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => SelectCard(card)));
|
||||||
|
builder.AddContent(sequence++, bestMatchName);
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
currentText = currentText.Substring(bestMatchIndex + bestMatchName.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AddContent(sequence++, currentText);
|
||||||
|
currentText = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,22 +96,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mini-card {
|
.mini-card {
|
||||||
width: 110px;
|
width: 100px;
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: border-color var(--transition), transform var(--transition);
|
transition: border-color var(--transition), transform var(--transition);
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 2.5 / 3.5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-card:hover {
|
.mini-card:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
transform: translateY(-2px);
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-card-img {
|
.mini-card-img {
|
||||||
width: 110px;
|
flex: 1;
|
||||||
height: 100px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -122,18 +125,52 @@
|
|||||||
.mini-card-img img {
|
.mini-card-img img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-card-name {
|
.mini-card-name {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0.35rem 0.4rem;
|
padding: 0.25rem 0.3rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.deck-description {
|
||||||
@@ -142,6 +179,7 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.deck-description p {
|
.deck-description p {
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 1rem;
|
||||||
}
|
}
|
||||||
@@ -162,229 +200,4 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Detail Modal (shared with Cards page) ── */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1040;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
animation: fade-in 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-in {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 1050;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 0;
|
|
||||||
max-width: 720px;
|
|
||||||
width: 92vw;
|
|
||||||
max-height: 88vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
|
||||||
color: var(--text-primary);
|
|
||||||
animation: detail-enter 0.25s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes detail-enter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(-50%, -50%) scale(0.92);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(-50%, -50%) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.75rem;
|
|
||||||
right: 0.75rem;
|
|
||||||
z-index: 1;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
color: var(--text-primary);
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: all var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-close:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-image {
|
|
||||||
flex: 0 0 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-image img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header h2 {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.25rem 0.6rem;
|
|
||||||
border-radius: 100px;
|
|
||||||
background: var(--bg-hover);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category.agent {
|
|
||||||
background: rgba(79, 195, 247, 0.15);
|
|
||||||
color: #4fc3f7;
|
|
||||||
border-color: rgba(79, 195, 247, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category.spell {
|
|
||||||
background: rgba(206, 147, 216, 0.15);
|
|
||||||
color: #ce93d8;
|
|
||||||
border-color: rgba(206, 147, 216, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.category.token {
|
|
||||||
background: rgba(255, 213, 79, 0.15);
|
|
||||||
color: #ffd54f;
|
|
||||||
border-color: rgba(255, 213, 79, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.cost {
|
|
||||||
background: rgba(255, 215, 0, 0.12);
|
|
||||||
color: var(--gold);
|
|
||||||
border-color: rgba(255, 215, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.attack {
|
|
||||||
background: rgba(239, 83, 80, 0.15);
|
|
||||||
color: #ef5350;
|
|
||||||
border-color: rgba(239, 83, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.health {
|
|
||||||
background: rgba(102, 187, 106, 0.15);
|
|
||||||
color: #66bb6a;
|
|
||||||
border-color: rgba(102, 187, 106, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-badge.speed {
|
|
||||||
background: rgba(79, 195, 247, 0.12);
|
|
||||||
color: #4fc3f7;
|
|
||||||
border-color: rgba(79, 195, 247, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field.description {
|
|
||||||
background: var(--bg-elevated);
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border-left: 3px solid var(--accent);
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 7.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field.description .field-label {
|
|
||||||
min-width: auto;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-value {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-field.description .field-value {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.detail-layout {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-image {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
max-width: 180px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-detail {
|
|
||||||
max-height: 90vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<div class="decks-header">
|
<div class="decks-header">
|
||||||
<h1>Decks</h1>
|
<h1>Decks</h1>
|
||||||
<p class="text-secondary">@decks.Count deck@(decks.Count != 1 ? "s" : "")</p>
|
<p class="text-secondary">@decks.Count deck@(decks.Count != 1 ? "s" : "")</p>
|
||||||
|
<p class="text-primary">Please note these are my crafted decks. They are probably not optimal or good. See <a href="https://runeterra.ar/chrono/home">Runeterra.ar</a> and check out the decks from recognizable names there. This is not an ad, just common sense.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="deck-list">
|
<div class="deck-list">
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="deck-card-meta">
|
<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)
|
@if (deck.Keycards.Count > 0)
|
||||||
{
|
{
|
||||||
<span>@deck.Keycards.Count keycard@(deck.Keycards.Count != 1 ? "s" : "")</span>
|
<span>@deck.Keycards.Count keycard@(deck.Keycards.Count != 1 ? "s" : "")</span>
|
||||||
@@ -66,4 +67,5 @@
|
|||||||
var lastSpace = text.LastIndexOf(' ', maxLength);
|
var lastSpace = text.LastIndexOf(' ', maxLength);
|
||||||
return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "...";
|
return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-101
@@ -5,7 +5,6 @@
|
|||||||
<div class="home-hero">
|
<div class="home-hero">
|
||||||
<div class="hero-glow"></div>
|
<div class="hero-glow"></div>
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-badge">Collectible Card Game</div>
|
|
||||||
<h1 class="hero-title">Slop Game Reference - Chrono CCG</h1>
|
<h1 class="hero-title">Slop Game Reference - Chrono CCG</h1>
|
||||||
<p class="hero-subtitle">
|
<p class="hero-subtitle">
|
||||||
AI generated website for reference notes on playing Chrono CCG.
|
AI generated website for reference notes on playing Chrono CCG.
|
||||||
@@ -14,110 +13,13 @@
|
|||||||
<a class="cta-button primary" href="/cards">
|
<a class="cta-button primary" href="/cards">
|
||||||
<i class="bi bi-collection-fill"></i> Browse Cards
|
<i class="bi bi-collection-fill"></i> Browse Cards
|
||||||
</a>
|
</a>
|
||||||
|
<a class="cta-button secondary" href="/agents">
|
||||||
|
<i class="bi bi-people-fill"></i> Agents Data Table
|
||||||
|
</a>
|
||||||
<a class="cta-button secondary" href="/decks">
|
<a class="cta-button secondary" href="/decks">
|
||||||
<i class="bi bi-journal-text"></i> View Decks
|
<i class="bi bi-journal-text"></i> View Decks
|
||||||
</a>
|
</a>
|
||||||
<a class="cta-button secondary" href="/agents">
|
|
||||||
<i class="bi bi-people-fill"></i> All Agents
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="home-stats">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon agents">
|
|
||||||
<i class="bi bi-person-fill"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<span class="stat-value">@agentsCount</span>
|
|
||||||
<span class="stat-label">Agents</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon spells">
|
|
||||||
<i class="bi bi-wand"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<span class="stat-value">@spellsCount</span>
|
|
||||||
<span class="stat-label">Spells</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon tokens">
|
|
||||||
<i class="bi bi-coin"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<span class="stat-value">@tokensCount</span>
|
|
||||||
<span class="stat-label">Tokens</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon decks">
|
|
||||||
<i class="bi bi-journal-text"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<span class="stat-value">@visibleDecks</span>
|
|
||||||
<span class="stat-label">Decks</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon factions">
|
|
||||||
<i class="bi bi-shield-fill"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<span class="stat-value">@factionCount</span>
|
|
||||||
<span class="stat-label">Factions</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon total">
|
|
||||||
<i class="bi bi-grid-3x3-gap-fill"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-body">
|
|
||||||
<span class="stat-value">@totalCount</span>
|
|
||||||
<span class="stat-label">Total Cards</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-sections">
|
|
||||||
<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>
|
|
||||||
<a href="/cards" class="section-link">Browse Gallery <i class="bi bi-arrow-right"></i></a>
|
|
||||||
</div>
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-icon"><i class="bi bi-people-fill"></i></div>
|
|
||||||
<h2>View Agents</h2>
|
|
||||||
<p>All @agentsCount agents in a sortable data grid with filtering, paging, and detailed stats at a glance.</p>
|
|
||||||
<a href="/agents" class="section-link">View Agents <i class="bi bi-arrow-right"></i></a>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<a href="/decks" class="section-link">Browse Decks <i class="bi bi-arrow-right"></i></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private int totalCount;
|
|
||||||
private int agentsCount;
|
|
||||||
private int spellsCount;
|
|
||||||
private int tokensCount;
|
|
||||||
private int visibleDecks;
|
|
||||||
private int factionCount;
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
var cards = CardDatabase.Cards;
|
|
||||||
totalCount = cards.Count;
|
|
||||||
agentsCount = cards.Count(c => c.IsAgent);
|
|
||||||
spellsCount = cards.Count(c => c.IsSpell);
|
|
||||||
tokensCount = cards.Count(c => c.IsToken);
|
|
||||||
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;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon.agents { background: rgba(108, 99, 255, 0.15); color: var(--accent); }
|
.stat-icon.agents {
|
||||||
.stat-icon.spells { background: rgba(255, 215, 0, 0.12); color: var(--gold); }
|
background: rgba(108, 99, 255, 0.15);
|
||||||
.stat-icon.tokens { background: rgba(0, 200, 150, 0.15); color: #00c896; }
|
color: var(--accent);
|
||||||
.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.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 {
|
.stat-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -234,15 +257,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
@@media (max-width: 900px) {
|
@
|
||||||
.home-stats { grid-template-columns: repeat(3, 1fr); }
|
@media (max-width: 900px) {
|
||||||
.home-sections { grid-template-columns: 1fr; }
|
.home-stats {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-sections {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@media (max-width: 600px) {
|
@
|
||||||
.hero-title { font-size: 2.2rem; }
|
@media (max-width: 600px) {
|
||||||
.hero-subtitle { font-size: 0.95rem; }
|
.hero-title {
|
||||||
.hero-actions { flex-direction: column; align-items: center; }
|
font-size: 2.2rem;
|
||||||
.cta-button { width: 100%; justify-content: center; }
|
}
|
||||||
.home-stats { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Web
|
@using Web
|
||||||
@using Web.Layout
|
@using Web.Layout
|
||||||
|
@using Chrono.Components
|
||||||
@using Telerik.Blazor
|
@using Telerik.Blazor
|
||||||
@using Telerik.Blazor.Components
|
@using Telerik.Blazor.Components
|
||||||
@using Telerik.DataSource
|
@using Telerik.DataSource
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
@@ -7,6 +7,7 @@
|
|||||||
--text-secondary: #9898b8;
|
--text-secondary: #9898b8;
|
||||||
--text-muted: #686888;
|
--text-muted: #686888;
|
||||||
--accent: #6c63ff;
|
--accent: #6c63ff;
|
||||||
|
--accent-rgb: 108, 99, 255;
|
||||||
--accent-glow: rgba(108, 99, 255, 0.3);
|
--accent-glow: rgba(108, 99, 255, 0.3);
|
||||||
--gold: #ffd700;
|
--gold: #ffd700;
|
||||||
--gold-glow: rgba(255, 215, 0, 0.4);
|
--gold-glow: rgba(255, 215, 0, 0.4);
|
||||||
@@ -172,8 +173,14 @@ a, .btn-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loading-pulse {
|
@keyframes loading-pulse {
|
||||||
0%, 100% { transform: scale(1); opacity: 0.7; }
|
0%, 100% {
|
||||||
50% { transform: scale(1.15); opacity: 1; }
|
transform: scale(1);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-title {
|
.loading-title {
|
||||||
@@ -248,3 +255,29 @@ a, .btn-link {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.inline-card-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px dashed var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition);
|
||||||
|
display: inline;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-card-btn:hover {
|
||||||
|
color: var(--accent-light, #60a5fa);
|
||||||
|
border-bottom-style: solid;
|
||||||
|
background-color: rgba(var(--accent-rgb, 59, 130, 246), 0.1);
|
||||||
|
}
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||||
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect"/>
|
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"/>
|
<link href="/lib/bootstrap-icons/bootstrap-icons.min.css" rel="stylesheet"/>
|
||||||
<link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
|
<link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
|
||||||
<link href="/_content/Telerik.UI.for.Blazor/css/kendo-theme-bootstrap/all.css" rel="stylesheet"/>
|
<link href="/_content/Telerik.UI.for.Blazor/css/kendo-theme-bootstrap/all.css" rel="stylesheet"/>
|
||||||
<link href="/css/app.css" rel="stylesheet"/>
|
<link href="/css/app.css" rel="stylesheet"/>
|
||||||
<link href="/Web.styles.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"/>
|
<link href="/favicon.png" rel="icon" type="image/png"/>
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -40,11 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
|
||||||
An unhandled error has occurred.
|
|
||||||
<a class="reload" href=".">Reload</a>
|
|
||||||
<span class="dismiss">🗙</span>
|
|
||||||
</div>
|
|
||||||
<script src="/_framework/blazor.webassembly.js"></script>
|
<script src="/_framework/blazor.webassembly.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"navigationFallback": {
|
||||||
|
"rewrite": "/index.html",
|
||||||
|
"exclude": ["/css/*", "/lib/*", "/_framework/*", "/_content/*", "/sample-data/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Chrono CCG - Project Guide
|
|
||||||
|
|
||||||
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**:
|
|
||||||
- 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).
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
- **Auto-Migrations**: The Server project automatically handles database schema updates on startup.
|
|
||||||
- **Dockerized Architecture**: Complete orchestration of the web server and database.
|
|
||||||
Vendored
+3
-1
@@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"alwaysUpdateLinks": true
|
||||||
|
}
|
||||||
+3
-1
@@ -1 +1,3 @@
|
|||||||
[]
|
[
|
||||||
|
"frontmatter-folder-organizer"
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";var d=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var w=(i,t)=>{for(var e in t)d(i,e,{get:t[e],enumerable:!0})},z=(i,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of p(t))!y.call(i,r)&&r!==e&&d(i,r,{get:()=>t[r],enumerable:!(n=F(t,r))||n.enumerable});return i};var S=i=>z(d({},"__esModule",{value:!0}),i);var b={};w(b,{default:()=>m});module.exports=S(b);var g=require("obsidian");var l=require("obsidian"),f={frontmatterHierarchy:"Category, Faction"},h=class extends l.PluginSettingTab{constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this;t.empty(),t.createEl("h2",{text:"Frontmatter Folder Organizer"}),new l.Setting(t).setName("Frontmatter hierarchy").setDesc("Enter frontmatter keys in order. Files will be organized into folders based on these values.").addTextArea(e=>e.setPlaceholder("category").setValue(this.plugin.settings.frontmatterHierarchy).onChange(async n=>{this.plugin.settings.frontmatterHierarchy=n,await this.plugin.saveSettings()})),new l.Setting(t).setName("Organize files now").setDesc("Create folders and move markdown files according to the configured frontmatter hierarchy.").addButton(e=>e.setButtonText("Organize now").onClick(async()=>{await this.plugin.organizeFilesByFrontmatter()}))}};var m=class extends g.Plugin{async onload(){await this.loadSettings(),this.addCommand({id:"organize-frontmatter-folders",name:"Organize markdown by frontmatter hierarchy",callback:async()=>{await this.organizeFilesByFrontmatter()}}),this.addSettingTab(new h(this.app,this))}async organizeFilesByFrontmatter(){let t=this.getHierarchyKeys();if(!t.length){new g.Notice("Set a frontmatter hierarchy in plugin settings before organizing files.");return}let e=this.app.vault.getFiles().filter(r=>r.extension==="md"),n=0;for(let r of e){let a=this.getFolderSegmentsForFile(r,t);if(!a||!a.length)continue;let s=a.join("/");if(`${s}/${r.name}`===r.path)continue;await this.ensureFolderExists(s);let c=await this.getAvailableTargetPath(s,r.name);await this.app.vault.rename(r,c),n+=1}new g.Notice(`Organized ${n} markdown file${n===1?"":"s"} by frontmatter folder hierarchy.`)}getHierarchyKeys(){return this.settings.frontmatterHierarchy.split(/[,\r\n]+/).map(t=>t.trim()).filter(Boolean)}getFolderSegmentsForFile(t,e){let n=this.app.metadataCache.getFileCache(t);if(!n?.frontmatter)return null;let r=[];for(let a of e){let s=this.getFrontmatterValueByKey(n.frontmatter,a),o=this.normalizeFrontmatterValue(s);if(o===null)break;r.push(this.sanitizeFolderName(o))}return r.length?r:null}getFrontmatterValueByKey(t,e){let n=e.trim().toLowerCase();for(let[r,a]of Object.entries(t))if(r.trim().toLowerCase()===n)return a}normalizeFrontmatterValue(t){if(t==null||typeof t=="object")return null;let e=String(t).trim();return e.length?e:null}sanitizeFolderName(t){return t.replace(/[<>:"/\\|?*]/g,"-").trim()}async ensureFolderExists(t){let e=t.replace(/\\/g,"/").split("/").filter(Boolean),n="";for(let r of e)n=n?`${n}/${r}`:r,this.app.vault.getAbstractFileByPath(n)||await this.app.vault.createFolder(n)}async getAvailableTargetPath(t,e){let n=t.replace(/\\/g,"/"),r=`${n}/${e}`;if(!this.app.vault.getAbstractFileByPath(r))return r;let a=e.lastIndexOf("."),s=a>=0?e.slice(0,a):e,o=a>=0?e.slice(a):"",c=1;for(;;){let u=`${n}/${s}-${c}${o}`;if(!this.app.vault.getAbstractFileByPath(u))return u;c+=1}}async loadSettings(){this.settings=Object.assign({},f,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "frontmatter-folder-organizer",
|
||||||
|
"name": "Frontmatter Folder Organizer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "1.0.0",
|
||||||
|
"description": "Organize markdown files into folders based on frontmatter hierarchy.",
|
||||||
|
"author": "Obsidian",
|
||||||
|
"authorUrl": "https://obsidian.md",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This CSS file will be included with your plugin, and
|
||||||
|
available in the app when your plugin is enabled.
|
||||||
|
|
||||||
|
If your plugin does not need CSS, delete this file.
|
||||||
|
|
||||||
|
*/
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "frontmatter-markdown-links",
|
|
||||||
"name": "Frontmatter Markdown Links",
|
|
||||||
"version": "2.6.34",
|
|
||||||
"description": "Adds support for markdown links in frontmatter.",
|
|
||||||
"author": "mnaoumov",
|
|
||||||
"authorUrl": "https://github.com/mnaoumov/",
|
|
||||||
"isDesktopOnly": false,
|
|
||||||
"minAppVersion": "1.12.7",
|
|
||||||
"fundingUrl": "https://www.buymeacoffee.com/mnaoumov"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.frontmatter-markdown-links.text-property-widget-component.metadata-property-value{gap:0}.frontmatter-markdown-links.text-property-widget-component.metadata-property-value>.metadata-property-value{--metadata-divider-width: 0}.frontmatter-markdown-links.text-property-widget-component.metadata-property-value>.metadata-property-value>.metadata-property-value:not(:first-of-type) .metadata-link,.frontmatter-markdown-links.text-property-widget-component.metadata-property-value>.metadata-property-value>.metadata-property-value:not(:first-of-type) .metadata-input-longtext{padding-left:0}.frontmatter-markdown-links.text-property-widget-component.metadata-property-value>.metadata-property-value>.metadata-property-value:not(:last-of-type) .metadata-link,.frontmatter-markdown-links.text-property-widget-component.metadata-property-value>.metadata-property-value>.metadata-property-value:not(:last-of-type) .metadata-input-longtext{padding-right:0}.frontmatter-markdown-links.text-property-widget-component.metadata-property-value .metadata-link-flair{display:none}.frontmatter-markdown-links .metadata-property-value,.frontmatter-markdown-links.multi-text-property-component{display:flex;flex:none;gap:0;white-space:pre}.bases-table-container:not(.mod-multiline) .frontmatter-markdown-links .metadata-input-longtext:not(:focus-within){white-space-collapse:preserve}
|
|
||||||
Vendored
+80
-52
@@ -8,17 +8,45 @@
|
|||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "c90153d5f925b0d5",
|
"id": "9aaa8a83f2165190",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Decks/Big Energy.md",
|
"file": "Spell/[[Lifeblood]]/Affront to Nature.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false
|
"source": false
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "Big Energy"
|
"title": "Affront to Nature"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6870293355431c6b",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "Factions/Singularity.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "Singularity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c17e1da0a76701dd",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "Factions/Singularity.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "Singularity"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -53,7 +81,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "search",
|
"type": "search",
|
||||||
"state": {
|
"state": {
|
||||||
"query": "",
|
"query": "The Ripper",
|
||||||
"matchingCase": false,
|
"matchingCase": false,
|
||||||
"explainSearch": false,
|
"explainSearch": false,
|
||||||
"collapseAll": false,
|
"collapseAll": false,
|
||||||
@@ -78,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
"width": 233.5
|
"width": 331.5
|
||||||
},
|
},
|
||||||
"right": {
|
"right": {
|
||||||
"id": "47f3bcd7aed212ab",
|
"id": "47f3bcd7aed212ab",
|
||||||
@@ -183,55 +211,55 @@
|
|||||||
"bases:Create new base": false
|
"bases:Create new base": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "c90153d5f925b0d5",
|
"active": "0ce4e65b7e2934e1",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"Swashbuckling Diehard.md",
|
"Timeline/[[Lifeblood]]/Deadly Fauna.md",
|
||||||
"Decks/Big Energy.md",
|
"Token/[[Singularity]]/Pocket Scout.md",
|
||||||
"_Decks.base",
|
"Keyword/Transient.md",
|
||||||
|
"Keyword/Sprout.md",
|
||||||
|
"Keyword/Shift.md",
|
||||||
|
"Keyword/Overpower.md",
|
||||||
|
"Keyword/Draw.md",
|
||||||
|
"Keyword/Flourish.md",
|
||||||
|
"Redirect/Cores.md",
|
||||||
|
"Redirect/Agents.md",
|
||||||
|
"Rule/Round End.md",
|
||||||
|
"Rule/Discarded.md",
|
||||||
|
"Rule/Chain.md",
|
||||||
|
"Set/Core Set.md",
|
||||||
|
"Speed/Fast.md",
|
||||||
|
"Speed/Immediate.md",
|
||||||
|
"Speed/Slow.md",
|
||||||
|
"Immortalized/[[Lifeblood]]/Arra, Saurian Broodmother.md",
|
||||||
|
"Immortalized/[[Lifeblood]]/Battleharts.md",
|
||||||
|
"Deck/Rewind Me.md",
|
||||||
|
"Deck/Big Energy.md",
|
||||||
"Decks/Rewind Me.canvas",
|
"Decks/Rewind Me.canvas",
|
||||||
"Decks/Rewind Me.md",
|
"images/Wolf.png",
|
||||||
"Overpower.md",
|
"images/Seedling.png",
|
||||||
"_Keyword.base",
|
"images/Pocket Scout.png",
|
||||||
"_Timeline.base",
|
"images/Unlocked Potential.png",
|
||||||
"Bronk the Calm.md",
|
"images/Throw into the Sun.png",
|
||||||
"Aggressive Recycling.md",
|
"images/Supernova.png",
|
||||||
"Agents.md",
|
"images/Sunshock.png",
|
||||||
"Affront to Nature.png",
|
"images/Soothing Glow.png",
|
||||||
"Aardvark Precinct Captain.png",
|
"images/Radiant Channeling.png",
|
||||||
"Aardvark Precinct Captain.md",
|
"images/Paradox Capacitor.png",
|
||||||
"A'kon, Starry Diviner.md",
|
"images",
|
||||||
"_Agents.base",
|
"Tokens",
|
||||||
"_Factions.base",
|
"Spells/Sungrace",
|
||||||
"_Spells.base",
|
"Spells/Singularity",
|
||||||
"Xae, Dreamstrider.md",
|
"Spells/Splintergleam",
|
||||||
"Ylka, the Headliner.md",
|
"Spells/Silence",
|
||||||
"Zealot of the Hunt.md",
|
"Spells/Phasetide",
|
||||||
"Zel, the First Diver.md",
|
"Spells/Lifeblood",
|
||||||
"Ziv, the Adaptable.md",
|
"Spells",
|
||||||
"Zorp, Unrecyclable.md",
|
"Agents/Singularity",
|
||||||
"Muffle.md",
|
"Spell/[[Lifeblood]]/Bloom.md",
|
||||||
"Mr. E.md",
|
"Token/[[Lifeblood]]/Seedling.md",
|
||||||
"Nanobot Hive.md",
|
"Token/[[Lifeblood]]/Wolf.md",
|
||||||
"Nascent Clone.md",
|
"Immortalized/[[Splintergleam]]/The Ripper.md",
|
||||||
"E-Law, Boot Shepherd.md",
|
"Agent/[[Splintergleam]]/Toothy Pugilist.md",
|
||||||
"Efficient Scrapbot.md",
|
|
||||||
"Egg Tender.md",
|
|
||||||
"Enhanced Retriever.md",
|
|
||||||
"Enlightened Refugee.md",
|
|
||||||
"Enlightened Survivor.md",
|
|
||||||
"Enthusiastic Bot-Poke.md",
|
|
||||||
"Fervent Follower.png",
|
|
||||||
"Fervent Mycologist.png",
|
|
||||||
"Bearer of the Broth.png",
|
|
||||||
"Battleharts.png",
|
|
||||||
"Appeal to the Scrolls.png",
|
|
||||||
"Zealot of the Hunt.png",
|
|
||||||
"Dedicated Missionary.png",
|
|
||||||
"Convergent Pack.png",
|
|
||||||
"Master of Ceremonies.png.crdownload",
|
|
||||||
"Master of Ceremonies_files/v833ccba57c9e4d2798f2e76cebdd09a11778172276447",
|
|
||||||
"Master of Ceremonies_files/flux.min.js.download",
|
|
||||||
"Master of Ceremonies_files/livewire.min.js.download",
|
|
||||||
"_Agents.canvas"
|
"_Agents.canvas"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+2
-1
@@ -14,7 +14,8 @@ archetypes:
|
|||||||
- "[[Sprout]]"
|
- "[[Sprout]]"
|
||||||
- "[[Shift]]"
|
- "[[Shift]]"
|
||||||
imageLink: "[[Aardvark Precinct Captain.png]]"
|
imageLink: "[[Aardvark Precinct Captain.png]]"
|
||||||
|
ranking: A
|
||||||
---
|
---
|
||||||
|
I feel like it easy to always make this card sprout 5, so +5/+5 worth of stats for two man. Plus at that point, it will Immortalize as a +3/+3 and activate [[Deadly Fauna]].
|
||||||
|
|
||||||
![[Aardvark Precinct Captain.png]]
|
![[Aardvark Precinct Captain.png]]
|
||||||
@@ -4,7 +4,7 @@ cost: 1
|
|||||||
attack: 0
|
attack: 0
|
||||||
health: 1
|
health: 1
|
||||||
description: "[[Activate]]: The next ally that enters play this round [[Flourish|Flourishes]]."
|
description: "[[Activate]]: The next ally that enters play this round [[Flourish|Flourishes]]."
|
||||||
immortalizeWhen: "I've seen allies [[Flourish]] 4+ times."
|
immortalizeWhen: I've seen allies [[Flourish]] 4+ times.
|
||||||
immortalizeTo:
|
immortalizeTo:
|
||||||
- "[[Arra, Saurian Broodmother]]"
|
- "[[Arra, Saurian Broodmother]]"
|
||||||
immortalizeFrom: N/A
|
immortalizeFrom: N/A
|
||||||
+5
-1
@@ -3,7 +3,7 @@ category: Agent
|
|||||||
cost: 6
|
cost: 6
|
||||||
attack: 6
|
attack: 6
|
||||||
health: 3
|
health: 3
|
||||||
description: "[[Blitz]]. I cost 1 less for each time you've [[Phased]] or [[Rewound]] an Agent this game. Strike: [[Rewind]] me.]]"
|
description: "[[Blitz]]. I cost 1 less for each time you've [[Phased]] or [[Rewound]] an Agent this game. Strike: [[Rewind]] me."
|
||||||
immortalizeWhen: You've [[Phased]] or [[Rewound]] Agents 6+ times this game.
|
immortalizeWhen: You've [[Phased]] or [[Rewound]] Agents 6+ times this game.
|
||||||
immortalizeTo:
|
immortalizeTo:
|
||||||
- "[[Masa, Time-Lost]]"
|
- "[[Masa, Time-Lost]]"
|
||||||
@@ -12,4 +12,8 @@ faction: "[[Phasetide]]"
|
|||||||
set: "[[Core Set]]"
|
set: "[[Core Set]]"
|
||||||
archetypes:
|
archetypes:
|
||||||
- "[[Rewound]]"
|
- "[[Rewound]]"
|
||||||
|
imageLink: "[[Divergence Assassin.png]]"
|
||||||
---
|
---
|
||||||
|
![[Divergence Assassin.png]]
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user