Vibed deck viewer
This commit is contained in:
@@ -2028,6 +2028,7 @@ public static class CardDatabase
|
||||
ImmortalizeTo = [
|
||||
],
|
||||
ImmortalizeFrom = "Gardener Apprentice",
|
||||
ImmortalizeWhen = "",
|
||||
},
|
||||
new()
|
||||
{
|
||||
@@ -2885,6 +2886,14 @@ public static class CardDatabase
|
||||
ImageFile = "Overmind's Guilt.png",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Overpower",
|
||||
Category = "Keyword",
|
||||
Description = "Excess damage beyond the Durability of this Agent's blocker is dealt directly to the enemy core.",
|
||||
Archetypes = [
|
||||
],
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Overseer of Trials",
|
||||
Category = "Agent",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// <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",
|
||||
"Kinetic Absorber",
|
||||
"Hidden Locus",
|
||||
"Suncursed Conduit",
|
||||
"Swashbuckling Diehard",
|
||||
"Debris Collector",
|
||||
"Paradox Flow",
|
||||
"Starfueled Medics",
|
||||
"Lumbering Starseeker",
|
||||
"Novathermal Mining",
|
||||
"Radiant Channeling",
|
||||
"Supernova",
|
||||
"Gunnery Captain",
|
||||
"Lightsteel Colossus",
|
||||
"Devourer Spawn",
|
||||
"Army of the Sun",
|
||||
],
|
||||
Keycards = [
|
||||
"Kinetic Absorber",
|
||||
"Hidden Locus",
|
||||
"Debris Collector",
|
||||
"Lumbering Starseeker",
|
||||
"Lightsteel Colossus",
|
||||
],
|
||||
Divers = [
|
||||
"Peaceful Synthesizer",
|
||||
"Limit Breaker",
|
||||
],
|
||||
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.",
|
||||
Factions = [
|
||||
"Sungrace",
|
||||
],
|
||||
IsVisible = true,
|
||||
},
|
||||
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,
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -24,6 +24,11 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
@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>
|
||||
@if (deck.Factions.Count > 0)
|
||||
{
|
||||
<div class="deck-factions">
|
||||
@foreach (var f in deck.Factions)
|
||||
{
|
||||
<span class="faction-badge">@f</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="deck-card-count">@deck.Cards.Count cards</p>
|
||||
</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 class="mini-card-name">@cardName</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 class="mini-card-name">@cardName</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 cardName in deck.Cards)
|
||||
{
|
||||
var card = LookupCard(cardName);
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
</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">
|
||||
@foreach (var paragraph in deck.Description.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
<p>@paragraph</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (selectedCard != null)
|
||||
{
|
||||
<div class="modal-backdrop" @onclick="CloseDetail"></div>
|
||||
<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 {
|
||||
[Parameter]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
private DeckData? deck;
|
||||
private CardData? selectedCard;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
.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: 110px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
}
|
||||
|
||||
.mini-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mini-card-img {
|
||||
width: 110px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mini-card-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.mini-card-name {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.35rem 0.4rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
@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>
|
||||
</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.Count card@(deck.Cards.Count != 1 ? "s" : "")</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;
|
||||
}
|
||||
Reference in New Issue
Block a user