Aggiunte skills per agente
This commit is contained in:
@@ -0,0 +1,822 @@
|
|||||||
|
---
|
||||||
|
name: blazor-specialist
|
||||||
|
description: Blazor UI development specialist. Use for building Blazor Server or WebAssembly apps, component development, state management, or form handling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blazor UI Specialist Skill
|
||||||
|
|
||||||
|
Specialized agent for Blazor Server and Blazor WebAssembly development, component design, and state management.
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
You are a Blazor UI Specialist responsible for building interactive web interfaces using Blazor, managing component lifecycle, implementing state management, handling forms and validation, and integrating with backend APIs.
|
||||||
|
|
||||||
|
## Expertise Areas
|
||||||
|
|
||||||
|
- Blazor Server architecture
|
||||||
|
- Blazor WebAssembly (WASM)
|
||||||
|
- Component lifecycle and rendering
|
||||||
|
- State management (FluxState, Fluxor)
|
||||||
|
- Form validation and submission
|
||||||
|
- JavaScript interop
|
||||||
|
- SignalR integration
|
||||||
|
- Component libraries (FluentUI, MudBlazor)
|
||||||
|
- Performance optimization
|
||||||
|
- Authentication in Blazor
|
||||||
|
- Responsive design patterns
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
1. **Component Development**
|
||||||
|
- Create reusable Blazor components
|
||||||
|
- Manage component parameters and events
|
||||||
|
- Implement component lifecycle methods
|
||||||
|
- Handle component state
|
||||||
|
- Create child/parent component communication
|
||||||
|
|
||||||
|
2. **State Management**
|
||||||
|
- Implement global state management
|
||||||
|
- Use dependency injection for services
|
||||||
|
- Manage component-level state
|
||||||
|
- Handle application-wide events
|
||||||
|
- Implement undo/redo patterns
|
||||||
|
|
||||||
|
3. **Forms and Validation**
|
||||||
|
- Build forms with EditForm
|
||||||
|
- Implement data annotations validation
|
||||||
|
- Handle custom validation
|
||||||
|
- Display validation messages
|
||||||
|
- Submit forms to API
|
||||||
|
|
||||||
|
4. **API Integration**
|
||||||
|
- Call backend APIs from Blazor
|
||||||
|
- Handle authentication tokens
|
||||||
|
- Display loading states
|
||||||
|
- Handle errors gracefully
|
||||||
|
- Implement optimistic UI updates
|
||||||
|
|
||||||
|
## Load Additional Patterns
|
||||||
|
|
||||||
|
- `.claude/patterns/api-patterns.md`
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Blazor Best Practices
|
||||||
|
- Use `@rendermode` appropriately (Server, WebAssembly, Auto)
|
||||||
|
- Dispose of resources in components (IDisposable)
|
||||||
|
- Avoid blocking the UI thread
|
||||||
|
- Use `StateHasChanged()` sparingly
|
||||||
|
- Minimize JavaScript interop
|
||||||
|
- Use cascading parameters for shared data
|
||||||
|
- Implement proper error boundaries
|
||||||
|
|
||||||
|
### Component Design
|
||||||
|
- Keep components focused (single responsibility)
|
||||||
|
- Use parameters for component inputs
|
||||||
|
- Use EventCallback for component outputs
|
||||||
|
- Make components reusable
|
||||||
|
- Separate presentation from logic
|
||||||
|
- Use code-behind for complex logic
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Use `@key` directive for list items
|
||||||
|
- Virtualize long lists
|
||||||
|
- Lazy load routes and components
|
||||||
|
- Minimize re-renders
|
||||||
|
- Use OnInitializedAsync for async initialization
|
||||||
|
- Stream large datasets
|
||||||
|
|
||||||
|
## Component Patterns
|
||||||
|
|
||||||
|
### Basic Component
|
||||||
|
```razor
|
||||||
|
@* File: Components/BudgetCard.razor *@
|
||||||
|
@namespace {ApplicationName}.UI.Components
|
||||||
|
|
||||||
|
<div class="budget-card">
|
||||||
|
<h3>@Budget.Name</h3>
|
||||||
|
<p class="amount">@Budget.Amount.ToString("C")</p>
|
||||||
|
<p class="created">Created: @Budget.CreatedDate.ToString("d")</p>
|
||||||
|
|
||||||
|
<button @onclick="OnEditClicked">Edit</button>
|
||||||
|
<button @onclick="OnDeleteClicked" class="danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public BudgetResponse Budget { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<BudgetResponse> OnEdit { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<Guid> OnDelete { get; set; }
|
||||||
|
|
||||||
|
private async Task OnEditClicked()
|
||||||
|
{
|
||||||
|
if (OnEdit.HasDelegate)
|
||||||
|
await OnEdit.InvokeAsync(Budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnDeleteClicked()
|
||||||
|
{
|
||||||
|
if (OnDelete.HasDelegate)
|
||||||
|
await OnDelete.InvokeAsync(Budget.BudgetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component with Code-Behind
|
||||||
|
```razor
|
||||||
|
@* File: Pages/Budgets/BudgetList.razor *@
|
||||||
|
@page "/budgets"
|
||||||
|
@namespace {ApplicationName}.UI.Pages.Budgets
|
||||||
|
@inherits BudgetListBase
|
||||||
|
|
||||||
|
<PageTitle>Budgets</PageTitle>
|
||||||
|
|
||||||
|
<div class="budget-list-page">
|
||||||
|
<h1>Budgets</h1>
|
||||||
|
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<p>Loading budgets...</p>
|
||||||
|
}
|
||||||
|
else if (ErrorMessage is not null)
|
||||||
|
{
|
||||||
|
<div class="error">@ErrorMessage</div>
|
||||||
|
}
|
||||||
|
else if (!Budgets.Any())
|
||||||
|
{
|
||||||
|
<p>No budgets found. Create your first budget!</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="budget-grid">
|
||||||
|
@foreach (var budget in Budgets)
|
||||||
|
{
|
||||||
|
<BudgetCard
|
||||||
|
Budget="@budget"
|
||||||
|
OnEdit="@HandleEditBudget"
|
||||||
|
OnDelete="@HandleDeleteBudget" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button @onclick="HandleCreateBudget" class="primary">Create Budget</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// File: Pages/Budgets/BudgetList.razor.cs
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using {ApplicationName}.UI.Services;
|
||||||
|
|
||||||
|
namespace {ApplicationName}.UI.Pages.Budgets;
|
||||||
|
|
||||||
|
public class BudgetListBase : ComponentBase, IDisposable
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
protected IBudgetService BudgetService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected NavigationManager Navigation { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected ILogger<BudgetListBase> Logger { get; set; } = default!;
|
||||||
|
|
||||||
|
protected List<BudgetResponse> Budgets { get; set; } = new();
|
||||||
|
protected bool IsLoading { get; set; }
|
||||||
|
protected string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadBudgetsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task LoadBudgetsAsync()
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
ErrorMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Budgets = await BudgetService.GetBudgetsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error loading budgets");
|
||||||
|
ErrorMessage = "Failed to load budgets. Please try again.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void HandleCreateBudget()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/budgets/create");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void HandleEditBudget(BudgetResponse budget)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/budgets/{budget.BudgetId}/edit");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task HandleDeleteBudget(Guid budgetId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await BudgetService.DeleteBudgetAsync(budgetId);
|
||||||
|
await LoadBudgetsAsync(); // Reload list
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error deleting budget {BudgetId}", budgetId);
|
||||||
|
ErrorMessage = "Failed to delete budget. Please try again.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Clean up subscriptions, timers, etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Validation Pattern
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@* File: Pages/Budgets/CreateBudget.razor *@
|
||||||
|
@page "/budgets/create"
|
||||||
|
@namespace {ApplicationName}.UI.Pages.Budgets
|
||||||
|
|
||||||
|
<PageTitle>Create Budget</PageTitle>
|
||||||
|
|
||||||
|
<div class="create-budget-page">
|
||||||
|
<h1>Create Budget</h1>
|
||||||
|
|
||||||
|
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" FormName="CreateBudgetForm">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name:</label>
|
||||||
|
<InputText id="name" @bind-Value="Model.Name" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => Model.Name)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="amount">Amount:</label>
|
||||||
|
<InputNumber id="amount" @bind-Value="Model.Amount" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => Model.Amount)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="startDate">Start Date:</label>
|
||||||
|
<InputDate id="startDate" @bind-Value="Model.StartDate" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => Model.StartDate)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" disabled="@IsSubmitting">
|
||||||
|
@if (IsSubmitting)
|
||||||
|
{
|
||||||
|
<span>Creating...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Create</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" @onclick="@Cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (ErrorMessage is not null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mt-3">@ErrorMessage</div>
|
||||||
|
}
|
||||||
|
</EditForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Inject]
|
||||||
|
private IBudgetService BudgetService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private NavigationManager Navigation { get; set; } = default!;
|
||||||
|
|
||||||
|
[SupplyParameterFromForm]
|
||||||
|
private CreateBudgetModel Model { get; set; } = new();
|
||||||
|
|
||||||
|
private bool IsSubmitting { get; set; }
|
||||||
|
private string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
private async Task HandleValidSubmit()
|
||||||
|
{
|
||||||
|
IsSubmitting = true;
|
||||||
|
ErrorMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var command = new CreateBudgetCommand(
|
||||||
|
Model.Name,
|
||||||
|
Model.Amount,
|
||||||
|
Model.StartDate);
|
||||||
|
|
||||||
|
var result = await BudgetService.CreateBudgetAsync(command);
|
||||||
|
|
||||||
|
Navigation.NavigateTo($"/budgets/{result.BudgetId}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorMessage = "Failed to create budget. Please try again.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/budgets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// File: Models/CreateBudgetModel.cs
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
public class CreateBudgetModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[StringLength(100, MinimumLength = 3)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Range(0.01, 1_000_000)]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTimeOffset StartDate { get; set; } = DateTimeOffset.Now;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management (Fluxor)
|
||||||
|
|
||||||
|
### Install Fluxor
|
||||||
|
```bash
|
||||||
|
dotnet add package Fluxor.Blazor.Web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Define State
|
||||||
|
```csharp
|
||||||
|
// File: Store/BudgetState/BudgetState.cs
|
||||||
|
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||||
|
|
||||||
|
public record BudgetState
|
||||||
|
{
|
||||||
|
public List<BudgetResponse> Budgets { get; init; } = new();
|
||||||
|
public bool IsLoading { get; init; }
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Define Actions
|
||||||
|
```csharp
|
||||||
|
// File: Store/BudgetState/BudgetActions.cs
|
||||||
|
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||||
|
|
||||||
|
public record LoadBudgetsAction;
|
||||||
|
public record LoadBudgetsSuccessAction(List<BudgetResponse> Budgets);
|
||||||
|
public record LoadBudgetsFailureAction(string ErrorMessage);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implement Reducer
|
||||||
|
```csharp
|
||||||
|
// File: Store/BudgetState/BudgetReducers.cs
|
||||||
|
using Fluxor;
|
||||||
|
|
||||||
|
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||||
|
|
||||||
|
public static class BudgetReducers
|
||||||
|
{
|
||||||
|
[ReducerMethod]
|
||||||
|
public static BudgetState ReduceLoadBudgetsAction(BudgetState state, LoadBudgetsAction action) =>
|
||||||
|
state with { IsLoading = true, ErrorMessage = null };
|
||||||
|
|
||||||
|
[ReducerMethod]
|
||||||
|
public static BudgetState ReduceLoadBudgetsSuccessAction(BudgetState state, LoadBudgetsSuccessAction action) =>
|
||||||
|
state with { IsLoading = false, Budgets = action.Budgets };
|
||||||
|
|
||||||
|
[ReducerMethod]
|
||||||
|
public static BudgetState ReduceLoadBudgetsFailureAction(BudgetState state, LoadBudgetsFailureAction action) =>
|
||||||
|
state with { IsLoading = false, ErrorMessage = action.ErrorMessage };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implement Effects
|
||||||
|
```csharp
|
||||||
|
// File: Store/BudgetState/BudgetEffects.cs
|
||||||
|
using Fluxor;
|
||||||
|
|
||||||
|
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||||
|
|
||||||
|
public class BudgetEffects(IBudgetService budgetService)
|
||||||
|
{
|
||||||
|
[EffectMethod]
|
||||||
|
public async Task HandleLoadBudgetsAction(LoadBudgetsAction action, IDispatcher dispatcher)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budgets = await budgetService.GetBudgetsAsync();
|
||||||
|
dispatcher.Dispatch(new LoadBudgetsSuccessAction(budgets));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
dispatcher.Dispatch(new LoadBudgetsFailureAction(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register Fluxor
|
||||||
|
```csharp
|
||||||
|
// Program.cs
|
||||||
|
builder.Services.AddFluxor(options =>
|
||||||
|
{
|
||||||
|
options.ScanAssemblies(typeof(Program).Assembly);
|
||||||
|
options.UseReduxDevTools();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use in Component
|
||||||
|
```razor
|
||||||
|
@inherits FluxorComponent
|
||||||
|
@inject IState<BudgetState> BudgetState
|
||||||
|
@inject IDispatcher Dispatcher
|
||||||
|
|
||||||
|
<div>
|
||||||
|
@if (BudgetState.Value.IsLoading)
|
||||||
|
{
|
||||||
|
<p>Loading...</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@foreach (var budget in BudgetState.Value.Budgets)
|
||||||
|
{
|
||||||
|
<BudgetCard Budget="@budget" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
Dispatcher.Dispatch(new LoadBudgetsAction());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Service Pattern
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// File: Services/BudgetService.cs
|
||||||
|
namespace {ApplicationName}.UI.Services;
|
||||||
|
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
public interface IBudgetService
|
||||||
|
{
|
||||||
|
Task<List<BudgetResponse>> GetBudgetsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<BudgetResponse> GetBudgetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
Task<CreateBudgetResponse> CreateBudgetAsync(CreateBudgetCommand command, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateBudgetAsync(Guid id, UpdateBudgetCommand command, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteBudgetAsync(Guid id, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BudgetService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<BudgetService> logger
|
||||||
|
) : IBudgetService
|
||||||
|
{
|
||||||
|
public async Task<List<BudgetResponse>> GetBudgetsAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budgets = await httpClient.GetFromJsonAsync<List<BudgetResponse>>(
|
||||||
|
"/budgets",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return budgets ?? new List<BudgetResponse>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error fetching budgets");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BudgetResponse> GetBudgetByIdAsync(
|
||||||
|
Guid id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budget = await httpClient.GetFromJsonAsync<BudgetResponse>(
|
||||||
|
$"/budgets/{id}",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return budget ?? throw new InvalidOperationException("Budget not found");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error fetching budget {BudgetId}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateBudgetResponse> CreateBudgetAsync(
|
||||||
|
CreateBudgetCommand command,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.PostAsJsonAsync(
|
||||||
|
"/budgets",
|
||||||
|
command,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<CreateBudgetResponse>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result ?? throw new InvalidOperationException("Failed to create budget");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error creating budget");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateBudgetAsync(
|
||||||
|
Guid id,
|
||||||
|
UpdateBudgetCommand command,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.PutAsJsonAsync(
|
||||||
|
$"/budgets/{id}",
|
||||||
|
command,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error updating budget {BudgetId}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteBudgetAsync(
|
||||||
|
Guid id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.DeleteAsync(
|
||||||
|
$"/budgets/{id}",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting budget {BudgetId}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register service
|
||||||
|
builder.Services.AddScoped<IBudgetService, BudgetService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Interop
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<button @onclick="ShowAlert">Show Alert</button>
|
||||||
|
<button @onclick="GetLocalStorage">Get from LocalStorage</button>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private async Task ShowAlert()
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetLocalStorage()
|
||||||
|
{
|
||||||
|
var value = await JS.InvokeAsync<string>("localStorage.getItem", "myKey");
|
||||||
|
Console.WriteLine($"Value from localStorage: {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetLocalStorage()
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("localStorage.setItem", "myKey", "myValue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// wwwroot/js/interop.js
|
||||||
|
window.budgetApp = {
|
||||||
|
formatCurrency: function (amount) {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD'
|
||||||
|
}).format(amount);
|
||||||
|
},
|
||||||
|
|
||||||
|
showConfirmDialog: function (message) {
|
||||||
|
return confirm(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadFile: function (filename, content) {
|
||||||
|
const blob = new Blob([content], { type: 'text/plain' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication in Blazor
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@* File: Pages/Login.razor *@
|
||||||
|
@page "/login"
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IAuthenticationService AuthService
|
||||||
|
|
||||||
|
<div class="login-page">
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
<EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email:</label>
|
||||||
|
<InputText @bind-Value="loginModel.Email" class="form-control" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password:</label>
|
||||||
|
<InputText type="password" @bind-Value="loginModel.Password" class="form-control" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</EditForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private LoginModel loginModel = new();
|
||||||
|
|
||||||
|
private async Task HandleLogin()
|
||||||
|
{
|
||||||
|
var result = await AuthService.LoginAsync(loginModel.Email, loginModel.Password);
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@* File: Components/AuthorizeView.razor *@
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<p>Hello, @context.User.Identity?.Name!</p>
|
||||||
|
<a href="/logout">Logout</a>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<a href="/login">Login</a>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Virtualization
|
||||||
|
```razor
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
|
||||||
|
<Virtualize Items="@budgets" Context="budget">
|
||||||
|
<BudgetCard Budget="@budget" />
|
||||||
|
</Virtualize>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading
|
||||||
|
```razor
|
||||||
|
@* File: App.razor *@
|
||||||
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<PageTitle>Not found</PageTitle>
|
||||||
|
<p>Sorry, there's nothing at this address.</p>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Blazor Pitfalls
|
||||||
|
|
||||||
|
### ❌ Avoid These Mistakes
|
||||||
|
|
||||||
|
1. **Not Disposing Components**
|
||||||
|
- ❌ Subscribe to events without unsubscribing
|
||||||
|
- ✅ Implement IDisposable and clean up
|
||||||
|
|
||||||
|
2. **Blocking UI Thread**
|
||||||
|
- ❌ Using `Task.Result` or `.Wait()`
|
||||||
|
- ✅ Always use `await`
|
||||||
|
|
||||||
|
3. **Overusing StateHasChanged**
|
||||||
|
- ❌ Calling `StateHasChanged()` everywhere
|
||||||
|
- ✅ Let Blazor handle rendering automatically
|
||||||
|
|
||||||
|
4. **Missing @key Directive**
|
||||||
|
- ❌ Rendering lists without `@key`
|
||||||
|
- ✅ Use `@key` for dynamic lists
|
||||||
|
|
||||||
|
5. **Not Handling Errors**
|
||||||
|
- ❌ No error boundaries
|
||||||
|
- ✅ Use ErrorBoundary component
|
||||||
|
|
||||||
|
## Blazor Checklist
|
||||||
|
|
||||||
|
### Component Development
|
||||||
|
- [ ] Components are focused and reusable
|
||||||
|
- [ ] Parameters use `[Parameter]` attribute
|
||||||
|
- [ ] EventCallbacks for component events
|
||||||
|
- [ ] Code-behind for complex logic
|
||||||
|
- [ ] IDisposable implemented where needed
|
||||||
|
|
||||||
|
### Forms & Validation
|
||||||
|
- [ ] EditForm used for forms
|
||||||
|
- [ ] Data annotations validation
|
||||||
|
- [ ] ValidationSummary displayed
|
||||||
|
- [ ] Submit button disabled during submission
|
||||||
|
- [ ] Error messages displayed
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- [ ] Global state managed (Fluxor/FluxState)
|
||||||
|
- [ ] Component state localized
|
||||||
|
- [ ] Services injected via DI
|
||||||
|
- [ ] State changes trigger re-renders
|
||||||
|
|
||||||
|
### API Integration
|
||||||
|
- [ ] HttpClient configured
|
||||||
|
- [ ] Loading states displayed
|
||||||
|
- [ ] Error handling implemented
|
||||||
|
- [ ] Authentication tokens included
|
||||||
|
- [ ] Retry logic for transient failures
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- [ ] Virtualization for long lists
|
||||||
|
- [ ] Lazy loading for routes
|
||||||
|
- [ ] @key directive on lists
|
||||||
|
- [ ] Minimal JavaScript interop
|
||||||
|
- [ ] Component disposal implemented
|
||||||
|
|
||||||
|
## Checklist Before Completion
|
||||||
|
|
||||||
|
- [ ] All components render correctly
|
||||||
|
- [ ] Forms validate and submit
|
||||||
|
- [ ] API calls successful
|
||||||
|
- [ ] Loading states displayed
|
||||||
|
- [ ] Error handling functional
|
||||||
|
- [ ] Authentication working
|
||||||
|
- [ ] State management functional
|
||||||
|
- [ ] Performance optimized
|
||||||
|
- [ ] Responsive design implemented
|
||||||
|
- [ ] Documentation complete
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
name: frontend-design
|
||||||
|
description: Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.
|
||||||
|
origin: ECC
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frontend Design
|
||||||
|
|
||||||
|
Use this when the task is not just "make it work" but "make it look designed."
|
||||||
|
|
||||||
|
This skill is for product pages, dashboards, app shells, components, or visual systems that need a clear point of view instead of generic AI-looking UI.
|
||||||
|
|
||||||
|
## When To Use
|
||||||
|
|
||||||
|
- building a landing page, dashboard, or app surface from scratch
|
||||||
|
- upgrading a bland interface into something intentional and memorable
|
||||||
|
- translating a product concept into a concrete visual direction
|
||||||
|
- implementing a frontend where typography, composition, and motion matter
|
||||||
|
|
||||||
|
## Core Principle
|
||||||
|
|
||||||
|
Pick a direction and commit to it.
|
||||||
|
|
||||||
|
Safe-average UI is usually worse than a strong, coherent aesthetic with a few bold choices.
|
||||||
|
|
||||||
|
## Design Workflow
|
||||||
|
|
||||||
|
### 1. Frame the interface first
|
||||||
|
|
||||||
|
Before coding, settle:
|
||||||
|
|
||||||
|
- purpose
|
||||||
|
- audience
|
||||||
|
- emotional tone
|
||||||
|
- visual direction
|
||||||
|
- one thing the user should remember
|
||||||
|
|
||||||
|
Possible directions:
|
||||||
|
|
||||||
|
- brutally minimal
|
||||||
|
- editorial
|
||||||
|
- industrial
|
||||||
|
- luxury
|
||||||
|
- playful
|
||||||
|
- geometric
|
||||||
|
- retro-futurist
|
||||||
|
- soft and organic
|
||||||
|
- maximalist
|
||||||
|
|
||||||
|
Do not mix directions casually. Choose one and execute it cleanly.
|
||||||
|
|
||||||
|
### 2. Build the visual system
|
||||||
|
|
||||||
|
Define:
|
||||||
|
|
||||||
|
- type hierarchy
|
||||||
|
- color variables
|
||||||
|
- spacing rhythm
|
||||||
|
- layout logic
|
||||||
|
- motion rules
|
||||||
|
- surface / border / shadow treatment
|
||||||
|
|
||||||
|
Use CSS variables or the project's token system so the interface stays coherent as it grows.
|
||||||
|
|
||||||
|
### 3. Compose with intention
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
- asymmetry when it sharpens hierarchy
|
||||||
|
- overlap when it creates depth
|
||||||
|
- strong whitespace when it clarifies focus
|
||||||
|
- dense layouts only when the product benefits from density
|
||||||
|
|
||||||
|
Avoid defaulting to a symmetrical card grid unless it is clearly the right fit.
|
||||||
|
|
||||||
|
### 4. Make motion meaningful
|
||||||
|
|
||||||
|
Use animation to:
|
||||||
|
|
||||||
|
- reveal hierarchy
|
||||||
|
- stage information
|
||||||
|
- reinforce user action
|
||||||
|
- create one or two memorable moments
|
||||||
|
|
||||||
|
Do not scatter generic micro-interactions everywhere. One well-directed load sequence is usually stronger than twenty random hover effects.
|
||||||
|
|
||||||
|
## Strong Defaults
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
- pick fonts with character
|
||||||
|
- pair a distinctive display face with a readable body face when appropriate
|
||||||
|
- avoid generic defaults when the page is design-led
|
||||||
|
|
||||||
|
### Color
|
||||||
|
|
||||||
|
- commit to a clear palette
|
||||||
|
- one dominant field with selective accents usually works better than evenly weighted rainbow palettes
|
||||||
|
- avoid cliché purple-gradient-on-white unless the product genuinely calls for it
|
||||||
|
|
||||||
|
### Background
|
||||||
|
|
||||||
|
Use atmosphere:
|
||||||
|
|
||||||
|
- gradients
|
||||||
|
- meshes
|
||||||
|
- textures
|
||||||
|
- subtle noise
|
||||||
|
- patterns
|
||||||
|
- layered transparency
|
||||||
|
|
||||||
|
Flat empty backgrounds are rarely the best answer for a product-facing page.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
- break the grid when the composition benefits from it
|
||||||
|
- use diagonals, offsets, and grouping intentionally
|
||||||
|
- keep reading flow obvious even when the layout is unconventional
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
Never default to:
|
||||||
|
|
||||||
|
- interchangeable SaaS hero sections
|
||||||
|
- generic card piles with no hierarchy
|
||||||
|
- random accent colors without a system
|
||||||
|
- placeholder-feeling typography
|
||||||
|
- motion that exists only because animation was easy to add
|
||||||
|
|
||||||
|
## Execution Rules
|
||||||
|
|
||||||
|
- preserve the established design system when working inside an existing product
|
||||||
|
- match technical complexity to the visual idea
|
||||||
|
- keep accessibility and responsiveness intact
|
||||||
|
- frontends should feel deliberate on desktop and mobile
|
||||||
|
|
||||||
|
## Quality Gate
|
||||||
|
|
||||||
|
Before delivering:
|
||||||
|
|
||||||
|
- the interface has a clear visual point of view
|
||||||
|
- typography and spacing feel intentional
|
||||||
|
- color and motion support the product instead of decorating it randomly
|
||||||
|
- the result does not read like generic AI UI
|
||||||
|
- the implementation is production-grade, not just visually interesting
|
||||||
Reference in New Issue
Block a user