Files
chapter-organizer/WebApp/Components/Pages/Notes.razor
T
poprhythm 5c4aaf91df Add Note and NoteHistory entities with configurations and service implementation
This commit introduces the Note and NoteHistory entities, along with their respective configurations for Entity Framework Core. The AppDbContext has been updated to include DbSet properties for both entities. A new INotesService interface and its implementation, NotesService, have been created to handle CRUD operations for notes, including history tracking. Additionally, several Blazor components have been added for note management, including dialogs for editing notes and viewing note history. The UI has been enhanced to support markdown content rendering and improved user interaction for note creation and editing. These changes contribute to a comprehensive note-taking feature within the application.
2026-01-15 21:47:01 -05:00

261 lines
8.0 KiB
Plaintext

@page "/notes"
@attribute [Authorize]
@implements IAsyncDisposable
@using Core.Entities
@using WebApp.Services
@using WebApp.Components.Shared.Components
@inject INotesService NotesService
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageHeader Title="Notes">
<ActionButtons>
<MudButton StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenCreateDialog"
Variant="Variant.Filled"
Color="Color.Primary">
Create Note
</MudButton>
</ActionButtons>
</PageHeader>
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
}
else if (!_notes.Any())
{
<MudText Typo="Typo.h6" Align="Align.Center" Class="my-8">
No notes yet. Create your first note to get started!
</MudText>
}
else
{
<MudExpansionPanels MultiExpansion="true">
@foreach (var note in _notes)
{
<MudExpansionPanel Text="@note.Title"
Icon="@Icons.Material.Filled.Note">
<MudStack Spacing="2">
@if (!string.IsNullOrWhiteSpace(note.Content))
{
<MudPaper Elevation="0" Class="pa-3" Style="background-color: var(--mud-palette-background-grey);">
<div class="markdown-content">
@((MarkupString)MarkdownHelper.ToHtml(note.Content))
</div>
</MudPaper>
}
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Color="Color.Secondary">
Last updated: @note.UpdatedAt.ToString("g") by @(note.LastModifiedBy ?? "Unknown")
</MudText>
<MudStack Row="true" Spacing="2">
<MudButton StartIcon="@Icons.Material.Filled.History"
OnClick="() => OpenHistoryDialog(note.Id)"
Variant="Variant.Text"
Size="Size.Small">
History
</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Edit"
OnClick="() => OpenEditDialog(note)"
Variant="Variant.Text"
Size="Size.Small"
Color="Color.Primary">
Edit
</MudButton>
<MudButton StartIcon="@Icons.Material.Outlined.Delete"
OnClick="() => DeleteNote(note)"
Variant="Variant.Text"
Size="Size.Small"
Color="Color.Error">
Delete
</MudButton>
</MudStack>
</MudStack>
</MudStack>
</MudExpansionPanel>
}
</MudExpansionPanels>
}
</MudPaper>
@code {
private List<Note> _notes = [];
private bool _isLoading = true;
private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false;
protected override void OnInitialized()
{
_cancellationTokenSource = new CancellationTokenSource();
}
protected override async Task OnInitializedAsync()
{
await LoadNotes();
}
private async Task LoadNotes()
{
if (_isDisposed) return;
_isLoading = true;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
_notes = (await NotesService.GetNotesAsync()).ToList();
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception ex)
{
if (!_isDisposed)
{
Snackbar.Add($"Error loading notes: {ex.Message}", Severity.Error);
}
}
finally
{
if (!_isDisposed)
{
_isLoading = false;
StateHasChanged();
}
}
}
private async Task OpenCreateDialog()
{
if (_isDisposed) return;
var parameters = new DialogParameters
{
["Note"] = new Note { Title = "", Content = "" },
["IsEdit"] = false
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Large,
FullWidth = true,
CloseButton = true
};
var dialog = await DialogService.ShowAsync<NoteEditDialog>("Create Note", parameters, options);
var result = await dialog.Result;
if (!result.Canceled && !_isDisposed)
{
await LoadNotes();
}
}
private async Task OpenEditDialog(Note note)
{
if (_isDisposed) return;
var parameters = new DialogParameters
{
["Note"] = note,
["IsEdit"] = true
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Large,
FullWidth = true,
CloseButton = true
};
var dialog = await DialogService.ShowAsync<NoteEditDialog>("Edit Note", parameters, options);
var result = await dialog.Result;
if (!result.Canceled && !_isDisposed)
{
await LoadNotes();
}
}
private async Task OpenHistoryDialog(int noteId)
{
if (_isDisposed) return;
var parameters = new DialogParameters
{
["NoteId"] = noteId
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Large,
FullWidth = true,
CloseButton = true
};
await DialogService.ShowAsync<NoteHistoryDialog>("Note History", parameters, options);
}
private async Task DeleteNote(Note note)
{
if (_isDisposed) return;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
var result = await DialogService.ShowMessageBox(
"Delete Note",
(MarkupString)$"Are you sure you want to delete <b>{note.Title}</b>? This cannot be undone.",
yesText: "Yes",
noText: "Cancel");
if (_isDisposed) return;
if (result == true)
{
await NotesService.DeleteNoteAsync(note.Id);
if (!_isDisposed)
{
Snackbar.Add($"Note '{note.Title}' deleted", Severity.Info);
await LoadNotes();
}
}
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception ex)
{
if (!_isDisposed)
{
Snackbar.Add($"Error deleting note: {ex.Message}", Severity.Error);
}
}
}
public async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
_isDisposed = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
await ValueTask.CompletedTask;
}
}