Files
ChronoCCG/Chrono/Web/Pages/MyDecks.razor
T
6d486f49 ffaab59585 ...
2026-09-09 17:19:03 -04:00

265 lines
11 KiB
Plaintext

@page "/my-decks"
@using Model
@using Web.Services
@inject UserDecksService UserDecksService
@inject AuthService AuthService
@inject NavigationManager Navigation
@inject NetworkStatusService NetworkStatus
@implements IDisposable
<PageTitle>Chrono CCG - My Decks</PageTitle>
<HeadContent>
<meta name="description" content="View and manage your saved Chrono CCG decks." />
</HeadContent>
<div class="my-decks-page container py-4">
@if (!AuthService.IsLoggedIn)
{
<div class="card bg-dark text-white border-secondary shadow p-5 text-center">
<i class="bi bi-shield-lock-fill text-warning mb-3" style="font-size: 3rem;"></i>
<h3 class="fw-bold mb-2">Authentication Required</h3>
<p class="text-secondary mb-4">Please log in to your Chrono CCG account to view and manage your saved decks.</p>
<div>
<a href="login" class="btn btn-warning fw-semibold px-4">
<i class="bi bi-box-arrow-in-right me-1"></i> Log In
</a>
</div>
</div>
}
else
{
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h1 class="h2 fw-bold mb-1 text-white">
<i class="bi bi-collection-play-fill text-warning me-2"></i> My Decks
</h1>
<p class="text-secondary mb-0">Manage and edit your custom PostgreSQL-persisted decks.</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" @onclick="LoadDecksAsync" disabled="@isLoading">
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
</button>
<a href="deck-builder" class="btn btn-primary btn-create-deck">
<i class="bi bi-plus-lg me-1"></i> Create Deck
</a>
</div>
</div>
@if (!string.IsNullOrEmpty(statusMessage))
{
<div class="alert @(statusIsError ? "alert-danger" : "alert-success") alert-dismissible fade show" role="alert">
<i class="bi @(statusIsError ? "bi-exclamation-triangle-fill" : "bi-check-circle-fill") me-2"></i>
@statusMessage
<button type="button" class="btn-close" @onclick="() => statusMessage = null"></button>
</div>
}
<div class="card bg-dark border-secondary p-3 mb-4">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div class="input-group" style="max-width: 400px;">
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
<input type="text" class="form-control bg-dark border-secondary text-white"
placeholder="Search decks by name, season, or notes..."
@bind="searchTerm" @bind:event="oninput" />
@if (!string.IsNullOrEmpty(searchTerm))
{
<button class="btn btn-outline-secondary" @onclick="() => searchTerm = string.Empty"><i class="bi bi-x-lg"></i></button>
}
</div>
<div class="text-secondary small">
Showing @FilteredDecks.Count deck(s)
</div>
</div>
</div>
@if (isLoading)
{
<div class="text-center py-5 text-secondary">
<div class="spinner-border text-primary mb-2" role="status"></div>
<div>Loading your saved decks...</div>
</div>
}
else if (!FilteredDecks.Any())
{
<div class="card bg-dark border-secondary text-center py-5 text-secondary">
<i class="bi bi-journal-x fs-1 d-block mb-3 text-muted"></i>
<h4 class="text-white">No Decks Found</h4>
<p class="small mb-4">@(string.IsNullOrEmpty(searchTerm) ? "You haven't built any decks yet." : "No decks match your search criteria.")</p>
<div>
<a href="deck-builder" class="btn btn-primary">
<i class="bi bi-hammer me-1"></i> Open Deck Builder
</a>
</div>
</div>
}
else
{
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
@foreach (var deck in FilteredDecks)
{
<div class="col" @key="deck.Id">
<div class="card deck-card bg-dark border-secondary h-100 shadow-sm">
<div class="card-header border-secondary d-flex align-items-start justify-content-between gap-2">
<div>
<h5 class="card-title deck-name fw-bold text-white mb-1">@deck.Name</h5>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-secondary">@(string.IsNullOrEmpty(deck.Season) ? "Season 1" : deck.Season)</span>
<span class="badge @(deck.IsValid ? "bg-success" : "bg-warning text-dark")">
@(deck.IsValid ? "Valid" : "Incomplete")
</span>
</div>
</div>
</div>
<div class="card-body">
<div class="deck-stats mb-3 d-flex gap-3 text-secondary small">
<div>
<i class="bi bi-stack text-primary me-1"></i>
<span class="text-white fw-semibold">@deck.Cards.Count</span>/40 cards
</div>
<div>
<i class="bi bi-shuffle text-warning me-1"></i>
<span class="text-white fw-semibold">@deck.Divers.Count</span>/2 divers
</div>
</div>
@if (deck.Divers.Count > 0)
{
<div class="mb-2">
<div class="text-secondary small fw-semibold mb-1">DIVERS</div>
<div class="d-flex flex-wrap gap-1">
@foreach (var diver in deck.Divers)
{
<span class="badge bg-dark border border-secondary text-info">@diver</span>
}
</div>
</div>
}
@if (!string.IsNullOrEmpty(deck.Notes))
{
<div class="deck-notes-preview bg-darker p-2 rounded text-secondary small mb-2 border border-secondary">
<div class="text-muted small fw-semibold">NOTES</div>
<div class="text-truncate">@deck.Notes</div>
</div>
}
<div class="text-muted small deck-updated-at mt-auto pt-2 border-top border-secondary">
<i class="bi bi-clock me-1"></i> Updated @deck.UpdatedAt.ToLocalTime().ToString("MMM dd, yyyy HH:mm")
</div>
</div>
<div class="card-footer border-secondary d-flex justify-content-between gap-2">
<a href="deck-builder/@deck.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil-square me-1"></i> Edit in Builder
</a>
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteDeckAsync(deck.Id)" disabled="@isDeleting">
<i class="bi bi-trash me-1"></i> Delete
</button>
</div>
</div>
</div>
}
</div>
}
}
</div>
@code {
private List<UserDeck> decks = [];
private string searchTerm = "";
private bool isLoading = true;
private bool isDeleting;
private string? statusMessage;
private bool statusIsError;
private List<UserDeck> FilteredDecks => string.IsNullOrWhiteSpace(searchTerm)
? decks
: decks.Where(d =>
(d.Name?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) ||
(d.Season?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) ||
(d.Notes?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) ||
d.Cards.Any(c => c.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) ||
d.Divers.Any(div => div.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
).ToList();
protected override async Task OnInitializedAsync()
{
AuthService.AuthStateChanged += OnAuthStateChanged;
await AuthService.InitializeAsync();
if (AuthService.IsLoggedIn)
{
await LoadDecksAsync();
}
else
{
isLoading = false;
}
}
private void OnAuthStateChanged()
{
_ = InvokeAsync(async () =>
{
if (AuthService.IsLoggedIn)
{
await LoadDecksAsync();
}
StateHasChanged();
});
}
private async Task LoadDecksAsync()
{
isLoading = true;
statusMessage = null;
try
{
decks = await UserDecksService.GetDecksAsync();
}
catch (Exception ex)
{
statusMessage = $"Failed to load decks: {ex.Message}";
statusIsError = true;
}
finally
{
isLoading = false;
}
}
private async Task DeleteDeckAsync(Guid id)
{
isDeleting = true;
statusMessage = null;
try
{
var success = await UserDecksService.DeleteDeckAsync(id);
if (success)
{
decks.RemoveAll(d => d.Id == id);
statusMessage = "Deck deleted successfully.";
statusIsError = false;
}
else
{
statusMessage = "Failed to delete deck.";
statusIsError = true;
}
}
catch (Exception ex)
{
statusMessage = $"Error deleting deck: {ex.Message}";
statusIsError = true;
}
finally
{
isDeleting = false;
}
}
public void Dispose()
{
AuthService.AuthStateChanged -= OnAuthStateChanged;
}
}