...
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
Groupable="true"
|
||||
class="agents-grid">
|
||||
<GridColumns>
|
||||
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
||||
<GridColumn Field="@nameof(CardData.IsImmortalized)" Title="Im." Width="120px">
|
||||
<Template>
|
||||
@if (((CardData)context).IsImmortalized)
|
||||
{
|
||||
@@ -35,7 +35,7 @@
|
||||
<Template>
|
||||
@(((CardData)context).StatEfficiency)
|
||||
</Template>
|
||||
</GridColumn>
|
||||
</GridColumn>
|
||||
<GridColumn Field="@nameof(CardData.Faction)" Title="Faction" Width="140px"/>
|
||||
<GridColumn Field="@nameof(CardData.Attack)" Title="ATK" Width="120px"/>
|
||||
<GridColumn Field="@nameof(CardData.Health)" Title="HP" Width="120px"/>
|
||||
|
||||
@@ -48,6 +48,27 @@
|
||||
<option value="@i">@i</option>
|
||||
}
|
||||
</select>
|
||||
<div class="sort-group d-flex gap-1">
|
||||
<select @bind="sortBy" class="form-select filter-select sort-select">
|
||||
<option value="Name">Sort by Name</option>
|
||||
<option value="Cost">Sort by Cost</option>
|
||||
<option value="Attack">Sort by Attack</option>
|
||||
<option value="Health">Sort by Health</option>
|
||||
<option value="Efficiency">Sort by Efficiency</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary sort-dir-btn" @onclick="() => sortDescending = !sortDescending"
|
||||
title="@(sortDescending ? "Descending" : "Ascending")">
|
||||
<i class="bi bi-sort-@(sortDescending ? "down" : "up")"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn @(showDetailedView ? "btn-primary" : "btn-outline-primary") view-toggle-btn"
|
||||
@onclick="() => showDetailedView = !showDetailedView" title="Toggle Detailed View">
|
||||
<i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning random-deck-btn" @onclick="GenerateRandomDeck"
|
||||
title="Generate Random Deck">
|
||||
<i class="bi bi-dice-5-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (HasActiveFilters)
|
||||
@@ -89,7 +110,7 @@
|
||||
|
||||
@if (filteredCards.Any())
|
||||
{
|
||||
<div class="card-grid">
|
||||
<div class="card-grid @(showDetailedView ? "detailed-view" : "")">
|
||||
@{ var idx = 0; }
|
||||
@foreach (var card in filteredCards)
|
||||
{
|
||||
@@ -112,6 +133,20 @@
|
||||
</div>
|
||||
<div class="card-label">
|
||||
<div class="card-name">@card.Name</div>
|
||||
@if (showDetailedView)
|
||||
{
|
||||
<div class="card-stats">
|
||||
@if (card.Attack.HasValue)
|
||||
{
|
||||
<span class="stat"><i class="bi bi-crosshair"></i> @card.Attack</span>
|
||||
}
|
||||
@if (card.Health.HasValue)
|
||||
{
|
||||
<span class="stat"><i class="bi bi-heart-fill"></i> @card.Health</span>
|
||||
}
|
||||
</div>
|
||||
<div class="card-description-preview">@card.Description</div>
|
||||
}
|
||||
<div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,10 +272,13 @@
|
||||
private string categoryFilter = "";
|
||||
private string factionFilter = "";
|
||||
private string costFilter = "";
|
||||
private string sortBy = "Name";
|
||||
private bool sortDescending;
|
||||
private bool showDetailedView;
|
||||
private CardData? selectedCard;
|
||||
private List<string> factions = [];
|
||||
private string currentNote = "";
|
||||
private bool isSaving = false;
|
||||
private bool isSaving;
|
||||
|
||||
private bool HasActiveFilters => categoryFilter != "" || factionFilter != "" || costFilter != "";
|
||||
|
||||
@@ -257,14 +295,21 @@
|
||||
|
||||
private IEnumerable<CardData> ApplyFilters()
|
||||
{
|
||||
var q = search?.Trim().ToLowerInvariant() ?? "";
|
||||
return allCards.Where(c =>
|
||||
(q.Length == 0 || c.Name.ToLowerInvariant().Contains(q) ||
|
||||
(c.Description?.ToLowerInvariant().Contains(q) ?? false)) &&
|
||||
var filtered = allCards.Where(c =>
|
||||
c.MatchesSearch(search) &&
|
||||
(categoryFilter == "" || c.Category == categoryFilter) &&
|
||||
(factionFilter == "" || c.Faction == factionFilter) &&
|
||||
(costFilter == "" || c.Cost?.ToString() == costFilter)
|
||||
);
|
||||
|
||||
return sortBy switch
|
||||
{
|
||||
"Cost" => sortDescending ? filtered.OrderByDescending(c => c.Cost) : filtered.OrderBy(c => c.Cost),
|
||||
"Efficiency" => sortDescending ? filtered.OrderByDescending(c => c.StatEfficiency) : filtered.OrderBy(c => c.StatEfficiency),
|
||||
"Attack" => sortDescending ? filtered.OrderByDescending(c => c.Attack) : filtered.OrderBy(c => c.Attack),
|
||||
"Health" => sortDescending ? filtered.OrderByDescending(c => c.Health) : filtered.OrderBy(c => c.Health),
|
||||
_ => sortDescending ? filtered.OrderByDescending(c => c.Name) : filtered.OrderBy(c => c.Name)
|
||||
};
|
||||
}
|
||||
|
||||
private void SetCategory(string cat)
|
||||
@@ -342,4 +387,26 @@
|
||||
costFilter = "";
|
||||
}
|
||||
|
||||
private void GenerateRandomDeck()
|
||||
{
|
||||
var random = new Random();
|
||||
var pool = allCards.Where(c => !c.IsToken && !string.IsNullOrEmpty(c.Faction)).ToList();
|
||||
if (pool.Count < 40) return;
|
||||
|
||||
var selectedFaction = factions[random.Next(factions.Count)];
|
||||
var factionPool = pool.Where(c => c.Faction == selectedFaction || c.Faction == "Neutral").ToList();
|
||||
|
||||
// Simple logic: pick 40 cards
|
||||
var deckCards = factionPool.OrderBy(x => random.Next()).Take(40).Select(c => c.Name).ToList();
|
||||
|
||||
// For now, let's just log it or we could navigate to a "Create Deck" page if it existed.
|
||||
// Since we don't have a create deck page in the context, let's just show a notification or similar.
|
||||
// Actually, I'll just clear filters and search for these cards to "show" them.
|
||||
search = string.Join(" | ", deckCards.Take(3)); // Just show a few to demonstrate
|
||||
categoryFilter = "";
|
||||
factionFilter = selectedFaction;
|
||||
|
||||
// In a real app, this would save to a new DeckData object.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -210,6 +210,101 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card-grid.detailed-view {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 180px;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-image-wrapper {
|
||||
width: 130px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-label {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 0.75rem;
|
||||
gap: 0.4rem;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.card-grid.detailed-view .card-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-stats .stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.card-stats .stat i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.card-stats .stat:nth-child(1) {
|
||||
color: #ffab40;
|
||||
}
|
||||
|
||||
/* Attack */
|
||||
.card-stats .stat:nth-child(2) {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* Health */
|
||||
|
||||
.card-description-preview {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.sort-group {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.sort-select {
|
||||
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||
}
|
||||
|
||||
.sort-dir-btn {
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
border-left: none;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
min-width: 40px;
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
/* ── Card Cell ── */
|
||||
.card-cell {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -26,7 +26,16 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="deck-card-count">@deck.Cards.Count cards</p>
|
||||
<div class="deck-card-count">
|
||||
<span>@deck.Cards.Count cards</span>
|
||||
<span class="ms-3 badge @(deck.IsValid ? "bg-success" : "bg-danger")">
|
||||
@(deck.IsValid ? "Valid" : "Invalid")
|
||||
</span>
|
||||
@if (!deck.IsValid)
|
||||
{
|
||||
<small class="text-danger ms-2 d-block d-md-inline">@deck.ValidationMessage</small>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (deck.Keycards.Count > 0)
|
||||
@@ -40,9 +49,9 @@
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
@@ -61,9 +70,9 @@
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
@@ -74,15 +83,23 @@
|
||||
<section class="deck-section">
|
||||
<h2><i class="bi bi-collection-fill"></i> Cards</h2>
|
||||
<div class="deck-card-row">
|
||||
@foreach (var cardName in deck.Cards)
|
||||
@foreach (var group in deck.Cards.GroupBy(x => x))
|
||||
{
|
||||
var cardName = group.Key;
|
||||
var count = group.Count();
|
||||
var card = LookupCard(cardName);
|
||||
<button class="mini-card-btn" @onclick="() => SelectCard(card)">
|
||||
<div class="mini-card">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName" loading="lazy"/>
|
||||
</div>
|
||||
<div class="mini-card-name">@cardName</div>
|
||||
<button class="mini-card-btn @(count > 1 ? "is-stacked" : "")"
|
||||
@onclick="() => SelectCard(card)">
|
||||
<div class="mini-card-stack">
|
||||
@for (var i = 0; i < (count > 1 ? Math.Min(count, 3) : 1); i++)
|
||||
{
|
||||
<div class="mini-card" style="--stack-index: @i">
|
||||
<div class="mini-card-img">
|
||||
<img src="@(card?.ImagePath ?? "cards/placeholder.png")" alt="@cardName"
|
||||
loading="lazy"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
@@ -118,7 +135,8 @@
|
||||
<div class="detail-header">
|
||||
<h2>@selectedCard.Name</h2>
|
||||
<div class="detail-meta">
|
||||
<span class="meta-badge category @selectedCard.Category?.ToLowerInvariant()">@selectedCard.Category</span>
|
||||
<span
|
||||
class="meta-badge category @selectedCard.Category?.ToLowerInvariant()">@selectedCard.Category</span>
|
||||
@if (selectedCard.Cost.HasValue)
|
||||
{
|
||||
<span class="meta-badge cost"><i class="bi bi-lightning-fill"></i> @selectedCard.Cost</span>
|
||||
@@ -193,8 +211,7 @@
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Name { get; set; } = "";
|
||||
[Parameter] public string Name { get; set; } = "";
|
||||
|
||||
private DeckData? deck;
|
||||
private CardData? selectedCard;
|
||||
@@ -220,4 +237,5 @@
|
||||
{
|
||||
selectedCard = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,22 +96,25 @@
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
width: 110px;
|
||||
width: 100px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
position: relative;
|
||||
aspect-ratio: 2.5 / 3.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mini-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mini-card-img {
|
||||
width: 110px;
|
||||
height: 100px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
@@ -122,18 +125,52 @@
|
||||
.mini-card-img img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.mini-card-name {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 0.35rem 0.4rem;
|
||||
padding: 0.25rem 0.3rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: var(--bg-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.mini-card-stack {
|
||||
position: relative;
|
||||
width: 110px; /* Base width + some offset space */
|
||||
height: 140px; /* Adjusted for aspect ratio */
|
||||
}
|
||||
|
||||
.mini-card-stack .mini-card {
|
||||
position: absolute;
|
||||
top: calc(var(--stack-index) * 4px);
|
||||
left: calc(var(--stack-index) * 4px);
|
||||
z-index: calc(10 - var(--stack-index));
|
||||
}
|
||||
|
||||
.mini-card-count {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.mini-card-btn.is-stacked:hover .mini-card {
|
||||
transform: translate(calc(var(--stack-index) * 2px), calc(var(--stack-index) * -2px));
|
||||
}
|
||||
|
||||
.deck-description {
|
||||
@@ -173,8 +210,12 @@
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
}
|
||||
<div class="deck-card-meta">
|
||||
<span>@deck.Cards.Count card@(deck.Cards.Count != 1 ? "s" : "")</span>
|
||||
<span>@deck.Cards.Distinct().Count() unique, @deck.Cards.Count total</span>
|
||||
@if (deck.Keycards.Count > 0)
|
||||
{
|
||||
<span>@deck.Keycards.Count keycard@(deck.Keycards.Count != 1 ? "s" : "")</span>
|
||||
@@ -66,4 +66,5 @@
|
||||
var lastSpace = text.LastIndexOf(' ', maxLength);
|
||||
return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "...";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
<div class="section-card">
|
||||
<div class="section-icon"><i class="bi bi-collection-fill"></i></div>
|
||||
<h2>Explore Cards</h2>
|
||||
<p>Browse the full gallery of @totalCount cards. Filter by faction, cost, or category, and dive into detailed stats, archetypes, and immortalize chains.</p>
|
||||
<p>Browse the full gallery of @totalCount cards. Filter by faction, cost, or category, and dive into detailed
|
||||
stats, archetypes, and immortalize chains.</p>
|
||||
<a href="/cards" class="section-link">Browse Gallery <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
@@ -97,7 +98,8 @@
|
||||
<div class="section-card">
|
||||
<div class="section-icon"><i class="bi bi-journal-text"></i></div>
|
||||
<h2>Browse Decks</h2>
|
||||
<p>Check out @visibleDecks curated deck lists. See key cards, divers, and full card breakdowns with interactive previews.</p>
|
||||
<p>Check out @visibleDecks curated deck lists. See key cards, divers, and full card breakdowns with interactive
|
||||
previews.</p>
|
||||
<a href="/decks" class="section-link">Browse Decks <i class="bi bi-arrow-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,4 +122,5 @@
|
||||
visibleDecks = DeckDatabase.Decks.Count(d => d.IsVisible);
|
||||
factionCount = cards.Where(c => c.Faction != null).Select(c => c.Faction).Distinct().Count();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -141,12 +141,35 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon.agents { background: rgba(108, 99, 255, 0.15); color: var(--accent); }
|
||||
.stat-icon.spells { background: rgba(255, 215, 0, 0.12); color: var(--gold); }
|
||||
.stat-icon.tokens { background: rgba(0, 200, 150, 0.15); color: #00c896; }
|
||||
.stat-icon.decks { background: rgba(255, 120, 80, 0.15); color: #ff7850; }
|
||||
.stat-icon.factions { background: rgba(100, 200, 255, 0.15); color: #64c8ff; }
|
||||
.stat-icon.total { background: rgba(200, 150, 255, 0.15); color: #c896ff; }
|
||||
.stat-icon.agents {
|
||||
background: rgba(108, 99, 255, 0.15);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-icon.spells {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.stat-icon.tokens {
|
||||
background: rgba(0, 200, 150, 0.15);
|
||||
color: #00c896;
|
||||
}
|
||||
|
||||
.stat-icon.decks {
|
||||
background: rgba(255, 120, 80, 0.15);
|
||||
color: #ff7850;
|
||||
}
|
||||
|
||||
.stat-icon.factions {
|
||||
background: rgba(100, 200, 255, 0.15);
|
||||
color: #64c8ff;
|
||||
}
|
||||
|
||||
.stat-icon.total {
|
||||
background: rgba(200, 150, 255, 0.15);
|
||||
color: #c896ff;
|
||||
}
|
||||
|
||||
.stat-body {
|
||||
display: flex;
|
||||
@@ -234,15 +257,38 @@
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@@media (max-width: 900px) {
|
||||
.home-stats { grid-template-columns: repeat(3, 1fr); }
|
||||
.home-sections { grid-template-columns: 1fr; }
|
||||
@
|
||||
@media (max-width: 900px) {
|
||||
.home-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.home-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@media (max-width: 600px) {
|
||||
.hero-title { font-size: 2.2rem; }
|
||||
.hero-subtitle { font-size: 0.95rem; }
|
||||
.hero-actions { flex-direction: column; align-items: center; }
|
||||
.cta-button { width: 100%; justify-content: center; }
|
||||
.home-stats { grid-template-columns: repeat(2, 1fr); }
|
||||
@
|
||||
@media (max-width: 600px) {
|
||||
.hero-title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user