377 lines
13 KiB
Plaintext
377 lines
13 KiB
Plaintext
@page "/card-notes"
|
|
@implements IDisposable
|
|
|
|
<PageTitle>Card Notes Management - Chrono DB</PageTitle>
|
|
|
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
<div>
|
|
<h2 class="fw-bold mb-1 text-white">Card Notes Database (`CardNote`)</h2>
|
|
<p class="text-secondary mb-0">Browse, add, edit, and delete notes associated with Chrono CCG cards.</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-chrono-secondary" @onclick="LoadNotesAsync">
|
|
<i class="bi bi-arrow-clockwise"></i> Refresh
|
|
</button>
|
|
<button class="btn btn-chrono-primary" @onclick="OpenAddModal">
|
|
<i class="bi bi-plus-lg"></i> Add New Note
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(alertMessage))
|
|
{
|
|
<div class="alert @(alertIsSuccess ? "alert-success" : "alert-danger") alert-dismissible fade show" role="alert">
|
|
<i class="bi @(alertIsSuccess ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill") me-2"></i>
|
|
@alertMessage
|
|
<button type="button" class="btn-close" @onclick="() => alertMessage = null"></button>
|
|
</div>
|
|
}
|
|
|
|
<div class="chrono-card">
|
|
<div class="d-flex align-items-center justify-content-between mb-3 gap-3">
|
|
<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 form-control-chrono"
|
|
placeholder="Search by card name or note content..."
|
|
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterNotes"/>
|
|
@if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
|
}
|
|
</div>
|
|
|
|
<div class="text-secondary small">
|
|
Showing @notes.Count note(s)
|
|
</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 card notes from database...</div>
|
|
</div>
|
|
}
|
|
else if (!notes.Any())
|
|
{
|
|
<div class="text-center py-5 text-secondary">
|
|
<i class="bi bi-journal-x fs-1 d-block mb-2"></i>
|
|
<h5>No Card Notes Found</h5>
|
|
<p class="small">There are currently no card notes matching your criteria.</p>
|
|
<div class="d-flex justify-content-center gap-2 mt-3">
|
|
<button class="btn btn-chrono-primary" @onclick="OpenAddModal">
|
|
<i class="bi bi-plus-lg"></i> Add First Note
|
|
</button>
|
|
<button class="btn btn-chrono-secondary" @onclick="SeedSampleNotesAsync">
|
|
<i class="bi bi-box-arrow-in-down"></i> Seed Sample Notes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="chrono-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 20%;">User</th>
|
|
<th style="width: 25%;">Card Name</th>
|
|
<th>Note Content</th>
|
|
<th style="width: 140px;" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var note in notes)
|
|
{
|
|
<tr>
|
|
<td>
|
|
<span
|
|
class="badge bg-secondary font-monospace">@(string.IsNullOrEmpty(note.Username) ? "global" : note.Username)</span>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold text-white d-flex align-items-center gap-2">
|
|
<i class="bi bi-card-heading text-primary"></i>
|
|
@note.CardName
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="text-light text-break">@note.Note</div>
|
|
</td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(note)"
|
|
title="Edit Note">
|
|
<i class="bi bi-pencil-fill"></i>
|
|
</button>
|
|
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(note)"
|
|
title="Delete Note">
|
|
<i class="bi bi-trash3-fill"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Add / Edit Modal -->
|
|
@if (showModal)
|
|
{
|
|
<div class="modal-backdrop-custom">
|
|
<div class="modal-dialog-custom">
|
|
<div class="modal-header-custom">
|
|
<h5 class="modal-title-custom">
|
|
<i class="bi @(isEditing ? "bi-pencil-square" : "bi-plus-circle") text-primary me-2"></i>
|
|
@(isEditing ? "Edit Card Note" : "Add New Card Note")
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
|
|
</div>
|
|
<div class="modal-body-custom">
|
|
<div class="mb-3">
|
|
<label class="form-label text-secondary small fw-bold">USERNAME</label>
|
|
@if (isEditing)
|
|
{
|
|
<input type="text" class="form-control form-control-chrono" value="@currentNote.Username"
|
|
disabled/>
|
|
}
|
|
else
|
|
{
|
|
<input type="text" class="form-control form-control-chrono"
|
|
placeholder="Enter username (e.g. player1)..." @bind="currentNote.Username"/>
|
|
<div class="form-text text-secondary">The username this note is associated with.</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label text-secondary small fw-bold">CARD NAME</label>
|
|
@if (isEditing)
|
|
{
|
|
<input type="text" class="form-control form-control-chrono" value="@currentNote.CardName"
|
|
disabled/>
|
|
<div class="form-text text-secondary">Card name acts as part of the primary key and cannot be
|
|
altered.
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<input type="text" class="form-control form-control-chrono" list="cardSuggestions"
|
|
placeholder="Enter or select card name..." @bind="currentNote.CardName"/>
|
|
<datalist id="cardSuggestions">
|
|
@foreach (var c in availableCardNames.Take(50))
|
|
{
|
|
<option value="@c"/>
|
|
}
|
|
</datalist>
|
|
<div class="form-text text-secondary">Select an existing card from the database or enter a
|
|
custom name.
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label text-secondary small fw-bold">STRATEGIC NOTE</label>
|
|
<textarea class="form-control form-control-chrono" rows="5"
|
|
placeholder="Enter strategic card observations, deck synergies, mulligan advice, or balancing notes..."
|
|
@bind="currentNote.Note"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer-custom">
|
|
<button type="button" class="btn btn-chrono-secondary" @onclick="CloseModal">Cancel</button>
|
|
<button type="button" class="btn btn-chrono-primary" @onclick="SaveNoteAsync" disabled="@isSaving">
|
|
@(isSaving ? "Saving..." : "Save Note")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
@if (showDeleteModal && noteToDelete != null)
|
|
{
|
|
<div class="modal-backdrop-custom">
|
|
<div class="modal-dialog-custom" style="max-width: 450px;">
|
|
<div class="modal-header-custom">
|
|
<h5 class="modal-title-custom text-danger">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
Confirm Delete Note
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white"
|
|
@onclick="() => showDeleteModal = false"></button>
|
|
</div>
|
|
<div class="modal-body-custom">
|
|
<p class="text-white mb-2">Are you sure you want to delete the note for:</p>
|
|
<div class="p-2 rounded bg-dark border border-secondary fw-bold text-primary mb-3">
|
|
@noteToDelete.CardName
|
|
</div>
|
|
<p class="text-secondary small mb-0">This action will remove the record from the database
|
|
permanently.</p>
|
|
</div>
|
|
<div class="modal-footer-custom">
|
|
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-chrono-danger" @onclick="ConfirmDeleteAsync" disabled="@isSaving">
|
|
@(isSaving ? "Deleting..." : "Delete Permanently")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private List<CardNote> notes = [];
|
|
private List<string> availableCardNames = [];
|
|
private string searchTerm = "";
|
|
private bool isLoading = true;
|
|
private bool isSaving;
|
|
private string? alertMessage;
|
|
private bool alertIsSuccess;
|
|
|
|
private bool showModal;
|
|
private bool isEditing;
|
|
private CardNote currentNote = new();
|
|
|
|
private bool showDeleteModal;
|
|
private CardNote? noteToDelete;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
|
|
|
try
|
|
{
|
|
availableCardNames = CardDatabase.Cards
|
|
.Select(c => c.Name)
|
|
.Where(n => !string.IsNullOrWhiteSpace(n))
|
|
.Distinct()
|
|
.OrderBy(n => n)
|
|
.ToList();
|
|
}
|
|
catch
|
|
{
|
|
availableCardNames = [];
|
|
}
|
|
|
|
await LoadNotesAsync();
|
|
}
|
|
|
|
private void HandleConfigChanged()
|
|
{
|
|
_ = LoadNotesAsync();
|
|
}
|
|
|
|
private async Task LoadNotesAsync()
|
|
{
|
|
isLoading = true;
|
|
try
|
|
{
|
|
notes = await DbService.GetCardNotesAsync(searchTerm);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
alertIsSuccess = false;
|
|
alertMessage = "Error loading card notes: " + ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
private async Task FilterNotes()
|
|
{
|
|
notes = await DbService.GetCardNotesAsync(searchTerm);
|
|
}
|
|
|
|
private async Task ClearSearch()
|
|
{
|
|
searchTerm = "";
|
|
await LoadNotesAsync();
|
|
}
|
|
|
|
private void OpenAddModal()
|
|
{
|
|
isEditing = false;
|
|
currentNote = new CardNote();
|
|
showModal = true;
|
|
}
|
|
|
|
private void OpenEditModal(CardNote note)
|
|
{
|
|
isEditing = true;
|
|
currentNote = new CardNote
|
|
{
|
|
Username = note.Username,
|
|
CardName = note.CardName,
|
|
Note = note.Note
|
|
};
|
|
showModal = true;
|
|
}
|
|
|
|
private void CloseModal()
|
|
{
|
|
showModal = false;
|
|
}
|
|
|
|
private async Task SaveNoteAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(currentNote.CardName))
|
|
{
|
|
alertIsSuccess = false;
|
|
alertMessage = "Card name cannot be empty.";
|
|
return;
|
|
}
|
|
|
|
isSaving = true;
|
|
var result = await DbService.SaveCardNoteAsync(currentNote, !isEditing);
|
|
isSaving = false;
|
|
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = result.Message;
|
|
|
|
if (result.Success)
|
|
{
|
|
showModal = false;
|
|
await LoadNotesAsync();
|
|
}
|
|
}
|
|
|
|
private void OpenDeleteModal(CardNote note)
|
|
{
|
|
noteToDelete = note;
|
|
showDeleteModal = true;
|
|
}
|
|
|
|
private async Task ConfirmDeleteAsync()
|
|
{
|
|
if (noteToDelete == null) return;
|
|
|
|
isSaving = true;
|
|
var result = await DbService.DeleteCardNoteAsync(noteToDelete.CardName, noteToDelete.Username);
|
|
isSaving = false;
|
|
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = result.Message;
|
|
showDeleteModal = false;
|
|
noteToDelete = null;
|
|
|
|
await LoadNotesAsync();
|
|
}
|
|
|
|
private async Task SeedSampleNotesAsync()
|
|
{
|
|
var result = await DbService.SeedSampleDataAsync();
|
|
alertIsSuccess = result.Success;
|
|
alertMessage = result.Message;
|
|
await LoadNotesAsync();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
|
}
|
|
|
|
}
|