...
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user