Reorganzing projects
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
// <auto-generated/>
|
||||
#nullable enable
|
||||
|
||||
namespace Chrono.Model;
|
||||
|
||||
public static class DeckDatabase
|
||||
{
|
||||
public static readonly System.Collections.Generic.List<DeckData> Decks =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = "Big Energy",
|
||||
Cards = [
|
||||
"Brilliant Martyr",
|
||||
"Brilliant Martyr",
|
||||
"Brilliant Martyr",
|
||||
"Kinetic Absorber",
|
||||
"Kinetic Absorber",
|
||||
"Kinetic Absorber",
|
||||
"Hidden Locus",
|
||||
"Hidden Locus",
|
||||
"Hidden Locus",
|
||||
"Suncursed Conduit",
|
||||
"Suncursed Conduit",
|
||||
"Suncursed Conduit",
|
||||
"Paradox Capacitor",
|
||||
"Paradox Capacitor",
|
||||
"Lumbering Starseeker",
|
||||
"Lumbering Starseeker",
|
||||
"Novathermal Mining",
|
||||
"Novathermal Mining",
|
||||
"Novathermal Mining",
|
||||
"Radiant Channeling",
|
||||
"Radiant Channeling",
|
||||
"Radiant Channeling",
|
||||
"Supernova",
|
||||
"Supernova",
|
||||
"Supernova",
|
||||
"Gunnery Captain",
|
||||
"Lightsteel Colossus",
|
||||
"Lightsteel Colossus",
|
||||
"Devourer Spawn",
|
||||
"Devourer Spawn",
|
||||
"Army of the Sun",
|
||||
"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 = [
|
||||
"Kinetic Absorber",
|
||||
"Lumbering Starseeker",
|
||||
"Lightsteel Colossus",
|
||||
"Curb the Anomalies",
|
||||
],
|
||||
Divers = [
|
||||
"Peaceful Synthesizer",
|
||||
"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.\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 = [
|
||||
"Sungrace",
|
||||
"Phasetide",
|
||||
],
|
||||
IsVisible = true,
|
||||
DeckCode = "AUHUE2LHEBCW4ZLSM54SAQ3POB4QWY3PNZZXI4TVMN2GKZBTAADACASPCICESCQCAUFDCAYGAMCAGAQDDYBQCAYBAOTQCAYQAMDQG",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Rewind Me",
|
||||
Cards = [
|
||||
"Curious Acolyte",
|
||||
"Chronicle of the One",
|
||||
"Prayer of Rescue",
|
||||
"Backhand",
|
||||
"Snap Back",
|
||||
"Sunbringer Artillerist",
|
||||
"Sunshock",
|
||||
"Temple Analyst",
|
||||
"Holy Cleaner",
|
||||
"Balance Blade",
|
||||
"Rescind Authorization",
|
||||
"Out of Line",
|
||||
"Divergence Assassin",
|
||||
"Chronal Quarantine",
|
||||
"Holder of the Instruments",
|
||||
],
|
||||
Keycards = [
|
||||
],
|
||||
Divers = [
|
||||
],
|
||||
Factions = [
|
||||
],
|
||||
IsVisible = false,
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@namespace Shared.Layout
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<article class="content px-4">
|
||||
<TelerikRootComponent>
|
||||
@Body
|
||||
</TelerikRootComponent>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
@namespace Shared.Layout
|
||||
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">
|
||||
<i class="bi bi-hourglass-split me-2 text-warning"></i> Chrono CCG
|
||||
</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<i class="bi bi-house-door-fill nav-icon"></i> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="cards">
|
||||
<i class="bi bi-collection-fill nav-icon"></i> Cards
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="agents">
|
||||
<i class="bi bi-people-fill nav-icon"></i> Agents
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="decks">
|
||||
<i class="bi bi-journal-text nav-icon"></i> Decks
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1.05rem;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255, 255, 255, 0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@namespace Shared.Pages
|
||||
@page "/agents"
|
||||
|
||||
<PageTitle>Agents</PageTitle>
|
||||
|
||||
<div class="agents-page">
|
||||
<TelerikGrid Data="@agents"
|
||||
Sortable="true"
|
||||
FilterMode="GridFilterMode.FilterRow"
|
||||
Pageable="true"
|
||||
PageSize="250"
|
||||
Resizable="true"
|
||||
Height="850px"
|
||||
Groupable="true"
|
||||
class="agents-grid">
|
||||
<GridColumns>
|
||||
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
||||
<Template>
|
||||
@if (((CardData)context).IsImmortalized)
|
||||
{
|
||||
<i class="bi bi-star-fill" style="color: #ffd700;" title="Immortalized"></i>
|
||||
}
|
||||
</Template>
|
||||
</GridColumn>
|
||||
<GridColumn Field="@nameof(CardData.Name)" Title="Card" Width="220px">
|
||||
<Template>
|
||||
<div class="agent-name-cell">
|
||||
<img src="@(((CardData)context).ImagePath)" alt="@(((CardData)context).Name)"
|
||||
class="agent-thumb"/>
|
||||
<span>@(((CardData)context).Name)</span>
|
||||
</div>
|
||||
</Template>
|
||||
</GridColumn>
|
||||
<GridColumn Field="@nameof(CardData.Cost)" Title="Cost" Width="120px"/>
|
||||
<GridColumn Field="@nameof(CardData.StatEfficiency)" Title="SE" Width="120px">
|
||||
<Template>
|
||||
@(((CardData)context).StatEfficiency)
|
||||
</Template>
|
||||
</GridColumn>
|
||||
<GridColumn Field="@nameof(CardData.Faction)" Title="Faction" Width="140px"/>
|
||||
<GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/>
|
||||
<GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/>
|
||||
<GridColumn Field="@nameof(CardData.Description)" Title="Description" Width="400px"/>
|
||||
|
||||
</GridColumns>
|
||||
</TelerikGrid>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<CardData> agents = [];
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agents = CardDatabase.Cards
|
||||
.Where(c => c.Category == "Agent")
|
||||
.OrderBy(c => c.Cost)
|
||||
.ThenBy(c => c.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
.agents-page {
|
||||
--kendo-color-app-surface: oklch(5.3% 0.013 270deg);
|
||||
--kendo-color-subtle: oklch(64% 0.018 270deg);
|
||||
--kendo-color-surface: oklch(10% 0.015 270deg);
|
||||
--kendo-color-border: oklch(25% 0.025 270deg / 0.7);
|
||||
--kendo-color-border-alt: oklch(30% 0.03 270deg / 0.5);
|
||||
--kendo-color-base: oklch(16% 0.02 270deg);
|
||||
--kendo-color-primary: oklch(55% 0.23 275deg);
|
||||
--kendo-color-secondary: oklch(35% 0 0deg);
|
||||
--kendo-color-tertiary: oklch(55% 0.15 240deg);
|
||||
--kendo-color-info: oklch(65% 0.15 250deg);
|
||||
--kendo-color-success: oklch(60% 0.15 140deg);
|
||||
--kendo-color-warning: oklch(75% 0.14 85deg);
|
||||
--kendo-color-error: oklch(55% 0.20 30deg);
|
||||
--kendo-color-inverse: oklch(85% 0.01 270deg);
|
||||
--kendo-color-series: oklch(55% 0.23 275deg);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid,
|
||||
.agents-page ::deep .k-grid-container,
|
||||
.agents-page ::deep .k-grid-content,
|
||||
.agents-page ::deep .k-grid-header-wrap {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid-header,
|
||||
.agents-page ::deep .k-grid-footer,
|
||||
.agents-page ::deep .k-pager-wrap {
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid-header .k-header,
|
||||
.agents-page ::deep .k-grid-header .k-column-title,
|
||||
.agents-page ::deep .k-grid-header .k-link,
|
||||
.agents-page ::deep .k-grid-header .k-grid-filter {
|
||||
color: #e8e8f0;
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid-header .k-link:hover,
|
||||
.agents-page ::deep .k-grid-header .k-grid-filter:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid td,
|
||||
.agents-page ::deep .k-grid-header th.k-header {
|
||||
border-color: oklch(25% 0.025 270deg / 0.7);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid tr.k-alt {
|
||||
background: oklch(12% 0.015 270deg);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid tr:hover {
|
||||
background: oklch(20% 0.025 270deg);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-grid-filter,
|
||||
.agents-page ::deep .k-grid-filter:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-input,
|
||||
.agents-page ::deep .k-picker {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-button-solid-base {
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-button-solid-base:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-pager-wrap {
|
||||
color: #e8e8f0;
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-pager-nav,
|
||||
.agents-page ::deep .k-pager-numbers,
|
||||
.agents-page ::deep .k-pager-numbers .k-link,
|
||||
.agents-page ::deep .k-pager-input,
|
||||
.agents-page ::deep .k-pager-info {
|
||||
color: #e8e8f0;
|
||||
}
|
||||
|
||||
.agents-page ::deep .k-pager-nav:hover,
|
||||
.agents-page ::deep .k-pager-numbers .k-link:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.agents-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.agents-header h1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.agents-header p {
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.agent-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.agent-thumb {
|
||||
width: 36px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
@namespace Shared.Pages
|
||||
@page "/decks/{Name}"
|
||||
|
||||
<PageTitle>@deck?.Name</PageTitle>
|
||||
|
||||
<div class="deck-detail-page">
|
||||
<a class="back-link" href="/decks"><i class="bi bi-arrow-left"></i> Back to Decks</a>
|
||||
|
||||
@if (deck == null)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<p>Deck not found.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<article class="deck-article">
|
||||
<header class="deck-header">
|
||||
<h1>@deck.Name</h1>
|
||||
<b>Deck Code:</b> <pre>@deck.DeckCode</pre>
|
||||
@if (deck.Factions.Count > 0)
|
||||
{
|
||||
<div class="deck-factions">
|
||||
@foreach (var f in deck.Factions)
|
||||
{
|
||||
<span class="faction-badge">@f</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="deck-card-count">
|
||||
<span>@deck.Cards.Count cards</span>
|
||||
<span class="ms-3 badge @(deck.IsValid ? "bg-success" : "bg-danger")">
|
||||
@(deck.IsValid ? "Valid" : "Invalid")
|
||||
</span>
|
||||
@if (!deck.IsValid)
|
||||
{
|
||||
<small class="text-danger ms-2 d-block d-md-inline">@deck.ValidationMessage</small>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (deck.Keycards.Count > 0)
|
||||
{
|
||||
<section class="deck-section">
|
||||
<h2><i class="bi bi-star-fill" style="color: var(--gold);"></i> Keycards</h2>
|
||||
<div class="deck-card-row">
|
||||
@foreach (var cardName in deck.Keycards)
|
||||
{
|
||||
var card = LookupCard(cardName);
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (deck.Divers.Count > 0)
|
||||
{
|
||||
<section class="deck-section">
|
||||
<h2><i class="bi bi-shuffle"></i> Divers</h2>
|
||||
<div class="deck-card-row">
|
||||
@foreach (var cardName in deck.Divers)
|
||||
{
|
||||
var card = LookupCard(cardName);
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="deck-section">
|
||||
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
|
||||
<div class="deck-card-row">
|
||||
@foreach (var group in deck.Cards.GroupBy(x => x))
|
||||
{
|
||||
var cardName = group.Key;
|
||||
var count = group.Count();
|
||||
var card = LookupCard(cardName);
|
||||
<button class="mini-card-btn @(count > 1 ? "is-stacked" : "")"
|
||||
@onclick="() => SelectCard(card)">
|
||||
<div class="mini-card-stack">
|
||||
@for (var i = 0; i < (count > 1 ? Math.Min(count, 3) : 1); i++)
|
||||
{
|
||||
<div class="mini-card" style="--stack-index: @i">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (deck.Description != null)
|
||||
{
|
||||
<section class="deck-section description">
|
||||
<h2><i class="bi bi-chat-quote-fill"></i> Description</h2>
|
||||
<div class="deck-description">
|
||||
@RenderDescription(deck.Description)
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (selectedCard != null)
|
||||
{
|
||||
<div class="detail-overlay" @onclick="CloseDetail"></div>
|
||||
<div class="card-detail @(!HasImage ? "card-detail-noimg" : "")">
|
||||
<button class="detail-close" @onclick="CloseDetail"><i class="bi bi-x-lg"></i></button>
|
||||
<div class="detail-layout @(!HasImage ? "layout-noimg" : "")">
|
||||
@if (HasImage)
|
||||
{
|
||||
<div class="detail-image">
|
||||
<img src="@selectedCard.ImagePath" alt="@selectedCard.Name"
|
||||
onerror="this.style.display='none';this.parentElement.style.display='none'" />
|
||||
</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">@RenderCardDescription(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">
|
||||
@foreach (var name in selectedCard.ImmortalizeTo!)
|
||||
{
|
||||
var targetName = name;
|
||||
var target = LookupCard(targetName);
|
||||
if (target != null)
|
||||
{
|
||||
<button class="inline-card-btn" @onclick="() => SelectCard(target)">@targetName</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
@targetName
|
||||
}
|
||||
}
|
||||
</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">
|
||||
@{
|
||||
var fromCard = LookupCard(selectedCard.ImmortalizeFrom);
|
||||
if (fromCard != null)
|
||||
{
|
||||
<button class="inline-card-btn" @onclick="() => SelectCard(fromCard)">@selectedCard.ImmortalizeFrom</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
@selectedCard.ImmortalizeFrom
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string Name { get; set; } = "";
|
||||
|
||||
private DeckData? deck;
|
||||
private CardData? selectedCard;
|
||||
|
||||
private bool HasImage => selectedCard?.ImageFile is { Length: > 0 } && selectedCard.ImageFile != "placeholder.png";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
var decoded = Uri.UnescapeDataString(Name);
|
||||
deck = DeckDatabase.Decks.FirstOrDefault(d => d.IsVisible && d.Name == decoded);
|
||||
}
|
||||
|
||||
private CardData? LookupCard(string cardName)
|
||||
{
|
||||
return CardDatabase.Cards.FirstOrDefault(c =>
|
||||
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private void SelectCard(CardData? card)
|
||||
{
|
||||
selectedCard = card;
|
||||
}
|
||||
|
||||
private void CloseDetail()
|
||||
{
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private RenderFragment RenderCardDescription(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, () => SelectCard(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,457 @@
|
||||
.deck-detail-page {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem 3rem;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.deck-article {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.deck-header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.deck-header h1 {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.deck-factions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.faction-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.deck-card-count {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.deck-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.deck-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.deck-section h2 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.deck-card-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.mini-card-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
width: 100px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
position: relative;
|
||||
aspect-ratio: 2.5 / 3.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mini-card:hover {
|
||||
border-color: var(--accent);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mini-card-img {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mini-card-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.mini-card-name {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.3rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--bg-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.mini-card-stack {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.7;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
.deck-description p {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.deck-description p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ── Card Detail Overlay (inline modal) ── */
|
||||
|
||||
.detail-overlay {
|
||||
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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
@namespace Shared.Pages
|
||||
@page "/decks"
|
||||
|
||||
<PageTitle>Decks</PageTitle>
|
||||
|
||||
<div class="decks-page">
|
||||
<div class="decks-header">
|
||||
<h1>Decks</h1>
|
||||
<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 class="deck-list">
|
||||
@foreach (var deck in decks)
|
||||
{
|
||||
<a class="deck-card" href="/decks/@Uri.EscapeDataString(deck.Name)">
|
||||
<div class="deck-card-body">
|
||||
<h2 class="deck-card-title">@deck.Name</h2>
|
||||
@if (deck.Factions.Count > 0)
|
||||
{
|
||||
<div class="deck-card-factions">
|
||||
@foreach (var f in deck.Factions)
|
||||
{
|
||||
<span class="faction-badge">@f</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="deck-card-meta">
|
||||
<span>@deck.Cards.Distinct().Count() unique, @deck.Cards.Count total</span>
|
||||
@if (deck.Keycards.Count > 0)
|
||||
{
|
||||
<span>@deck.Keycards.Count keycard@(deck.Keycards.Count != 1 ? "s" : "")</span>
|
||||
}
|
||||
@if (deck.Divers.Count > 0)
|
||||
{
|
||||
<span>@deck.Divers.Count diver@(deck.Divers.Count != 1 ? "s" : "")</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (deck.Description != null)
|
||||
{
|
||||
<div class="deck-card-excerpt">@Truncate(deck.Description, 120)</div>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (decks.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-journal-text"></i>
|
||||
<p>No decks available yet.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<DeckData> decks = [];
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
decks = DeckDatabase.Decks.Where(d => d.IsVisible).ToList();
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int maxLength)
|
||||
{
|
||||
if (text.Length <= maxLength) return text;
|
||||
var lastSpace = text.LastIndexOf(' ', maxLength);
|
||||
return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "...";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
.decks-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem 2rem;
|
||||
}
|
||||
|
||||
.decks-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.decks-header h1 {
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.deck-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.deck-card {
|
||||
display: block;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem 1.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.deck-card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.deck-card-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.deck-card-factions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.faction-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.15rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.deck-card-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.deck-card-meta span + span::before {
|
||||
content: "·";
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.deck-card-excerpt {
|
||||
margin-top: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@namespace Shared.Pages
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Slop Game Reference - Chrono CCG</PageTitle>
|
||||
|
||||
<div class="home-hero">
|
||||
<div class="hero-glow"></div>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">Slop Game Reference - Chrono CCG</h1>
|
||||
<p class="hero-subtitle">
|
||||
AI generated website for reference notes on playing Chrono CCG.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="cta-button primary" href="/cards">
|
||||
<i class="bi bi-collection-fill"></i> Browse Cards
|
||||
</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">
|
||||
<i class="bi bi-journal-text"></i> View Decks
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,294 @@
|
||||
/* ── Hero ── */
|
||||
.home-hero {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 4rem 1rem 3rem;
|
||||
margin-bottom: 2.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
top: -30%;
|
||||
left: 50%;
|
||||
width: 60rem;
|
||||
height: 30rem;
|
||||
transform: translateX(-50%);
|
||||
background: radial-gradient(ellipse at center, rgba(108, 99, 255, 0.15) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||
background: rgba(255, 215, 0, 0.06);
|
||||
padding: 0.3rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 1rem;
|
||||
line-height: 1.1;
|
||||
background: linear-gradient(135deg, #e8e8f0 0%, #6c63ff 50%, #ffd700 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.65;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 2rem;
|
||||
max-width: 34rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.7rem 1.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.cta-button.primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.cta-button.primary:hover {
|
||||
background: #7b73ff;
|
||||
box-shadow: 0 0 30px var(--accent-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cta-button.secondary {
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.cta-button.secondary:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── Stats ── */
|
||||
.home-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 0.75rem;
|
||||
max-width: 56rem;
|
||||
margin: 0 auto 3rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon.agents {
|
||||
background: rgba(108, 99, 255, 0.15);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-icon.spells {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.stat-icon.tokens {
|
||||
background: rgba(0, 200, 150, 0.15);
|
||||
color: #00c896;
|
||||
}
|
||||
|
||||
.stat-icon.decks {
|
||||
background: rgba(255, 120, 80, 0.15);
|
||||
color: #ff7850;
|
||||
}
|
||||
|
||||
.stat-icon.factions {
|
||||
background: rgba(100, 200, 255, 0.15);
|
||||
color: #64c8ff;
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: rgba(200, 150, 255, 0.15);
|
||||
color: #c896ff;
|
||||
}
|
||||
|
||||
.stat-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Section Cards ── */
|
||||
.home-sections {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.25rem;
|
||||
max-width: 56rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem 1.5rem;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.section-card:hover {
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: var(--radius);
|
||||
background: rgba(108, 99, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
color: var(--accent);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-card h2 {
|
||||
font-size: 1.15rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section-card p {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.section-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: gap var(--transition);
|
||||
}
|
||||
|
||||
.section-link:hover {
|
||||
gap: 0.6rem;
|
||||
color: #7b73ff;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@
|
||||
@media (max-width: 900px) {
|
||||
.home-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.home-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@
|
||||
@media (max-width: 600px) {
|
||||
.hero-title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@namespace Shared.Pages
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.9"/>
|
||||
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Generated files">
|
||||
<Compile Remove="Generated\**\*.cs"/>
|
||||
<Compile Include="Generated\**\*.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RunBuild" BeforeTargets="CoreCompile">
|
||||
<PropertyGroup>
|
||||
<_BuildProject>$(MSBuildThisFileDirectory)..\Build\Build.csproj</_BuildProject>
|
||||
</PropertyGroup>
|
||||
<Message Text="=== Running Build project (card metadata generation) ===" Importance="high"/>
|
||||
<Exec Command="dotnet run --project "$(_BuildProject)""/>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Generated\**\*.cs"/>
|
||||
<Compile Include="Generated\**\*.cs"/>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Chrono.Model
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using Shared.Layout
|
||||
@using Telerik.Blazor
|
||||
@using Telerik.Blazor.Components
|
||||
@using Telerik.DataSource
|
||||
Reference in New Issue
Block a user