Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 194 additions & 45 deletions src/ClaudeClothes.Web/Pages/ClothingItemDetails.razor
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
@page "/item/{id:int}"
@using ClaudeClothes.Core.Models
@using ClaudeClothes.Core.Interfaces
@using Microsoft.AspNetCore.Components.Web
@inject IReviewService ReviewService
@inject IClothingItemService ClothingItemService
@inject NavigationManager Navigation
@inject ILogger<ClothingItemDetails> Logger

<PageTitle>@(clothingItem?.Name ?? "Dettagli Capo") - ClaudeClothes</PageTitle>

<!-- Skip link for accessibility -->
<a href="#reviews-pagination" class="skip-link">Salta alla navigazione delle recensioni</a>

@if (clothingItem == null)
{
<div class="loading-page">
Expand Down Expand Up @@ -97,7 +101,7 @@ else
<span class="review-badge">@totalReviews</span>
</div>

@if (!reviews.Any())
@if (pagedReviews == null || !pagedReviews.Items.Any())
{
<div class="empty-reviews">
<i class="bi bi-chat-dots"></i>
Expand All @@ -108,7 +112,7 @@ else
else
{
<div class="reviews-list">
@foreach (var review in reviews)
@foreach (var review in pagedReviews.Items)
{
<div class="review-card">
<div class="review-header">
Expand Down Expand Up @@ -139,6 +143,36 @@ else
}
</div>
}

@if (pagedReviews != null && pagedReviews.TotalPages > 1)
{
<nav id="reviews-pagination"
class="pagination-controls"
role="navigation"
aria-label="Paginated reviews navigation"
@onkeydown="HandleReviewPageKeyDown">
<button class="pagination-btn"
@onclick="() => ChangeReviewPage(currentReviewPage - 1)"
disabled="@(!pagedReviews.HasPreviousPage)"
aria-label="Go to previous reviews page">
<i class="bi bi-chevron-left" aria-hidden="true"></i>
Precedente
</button>
<span class="pagination-info"
role="status"
aria-live="polite"
aria-atomic="true">
Pagina @currentReviewPage di @pagedReviews.TotalPages
</span>
<button class="pagination-btn"
@onclick="() => ChangeReviewPage(currentReviewPage + 1)"
disabled="@(!pagedReviews.HasNextPage)"
aria-label="Go to next reviews page">
Successivo
<i class="bi bi-chevron-right" aria-hidden="true"></i>
</button>
</nav>
}
</div>
</div>

Expand Down Expand Up @@ -240,6 +274,35 @@ else
}

<style>
/* Skip Link - WCAG 2.4.1 Bypass Blocks */
.skip-link {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
background: #2d3748;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0 0 8px 8px;
font-weight: 600;
text-decoration: none;
z-index: 9999;
}

.skip-link:focus {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 0;
width: auto;
height: auto;
overflow: visible;
outline: 3px solid #ff6b6b;
outline-offset: 2px;
}

/* Loading Page */
.loading-page {
min-height: 100vh;
Expand Down Expand Up @@ -847,63 +910,73 @@ else
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #5B8DBE, #7BA05B);
color: white;
border: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
padding: 0.85rem 1.75rem;
background: #0EAD69;
color: #FFFEF2;
border: 4px solid #1A1A1A;
border-radius: 0;
font-weight: 700;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(91, 141, 190, 0.3);
transition: all 0.2s ease;
box-shadow: 4px 4px 0 #1A1A1A;
position: relative;
text-transform: uppercase;
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 1px;
}

.pagination-btn:hover:not([disabled]) {
background: linear-gradient(135deg, #7BA05B, #95A3A6);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(91, 141, 190, 0.4);
background: #EE4266;
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0 #1A1A1A;
}

.pagination-btn:active:not([disabled]) {
transform: translate(0, 0);
box-shadow: 2px 2px 0 #1A1A1A;
}

/* Enhanced focus indicator for keyboard navigation - WCAG 2.1 AA compliant */
.pagination-btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(91, 141, 190, 0.4), 0 0 0 5px rgba(255, 255, 255, 1);
outline: 4px solid #1A1A1A;
outline-offset: 4px;
}

.pagination-btn:focus-visible {
outline: 3px solid #5B8DBE;
outline-offset: 2px;
box-shadow: 0 0 0 3px rgba(91, 141, 190, 0.4);
outline: 4px solid #1A1A1A;
outline-offset: 4px;
}

.pagination-btn[disabled] {
background: #cbd5e0;
color: #a0aec0;
background: #E0E0E0;
color: #666666;
cursor: not-allowed;
box-shadow: none;
box-shadow: 2px 2px 0 #1A1A1A;
opacity: 0.6;
}

/* Improved disabled state contrast - WCAG 2.1 AA minimum 3:1 */
/* Improved disabled state contrast - WCAG 2.1 AA compliant */
.pagination-btn[disabled]:focus {
outline: 2px solid #718096;
outline-offset: 2px;
box-shadow: 0 0 0 3px rgba(113, 128, 150, 0.3);
outline: 4px solid #1A1A1A;
outline-offset: 4px;
}

.pagination-btn i {
font-size: 1rem;
}

.pagination-info {
color: #4a5568;
font-weight: 600;
font-size: 1rem;
color: #1A1A1A;
font-weight: 900;
font-size: 1.2rem;
min-width: 140px;
text-align: center;
padding: 0.5rem;
padding: 0.5rem 1rem;
background: #FFD23F;
border: 4px solid #1A1A1A;
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 1px;
}

/* High contrast mode support */
Expand All @@ -920,11 +993,13 @@ else

/* Reduced motion support */
@@media (prefers-reduced-motion: reduce) {
.pagination-btn {
.pagination-btn,
.review-card {
transition: none;
}

.pagination-btn:hover:not([disabled]) {
.pagination-btn:hover:not([disabled]),
.review-card:hover {
transform: none;
}
}
Expand Down Expand Up @@ -972,14 +1047,14 @@ else

.pagination-btn {
padding: 0.625rem 1rem;
font-size: 0.9rem;
font-size: 1rem;
min-width: 44px; /* WCAG 2.1 AA minimum touch target size */
min-height: 44px;
}

.pagination-info {
min-width: 100px;
font-size: 0.9rem;
font-size: 1rem;
}
}
</style>
Expand All @@ -988,33 +1063,106 @@ else
[Parameter]
public int Id { get; set; }

[SupplyParameterFromQuery(Name = "reviewPage")]
public int? ReviewPage { get; set; }

private ClothingItem? clothingItem;
private List<Review> reviews = new();
private PagedResult<Review>? pagedReviews;
private Review newReview = new();
private string successMessage = string.Empty;
private bool isSubmitting = false;
private string errorMessage = string.Empty;
private int totalReviews => reviews.Count;
private int currentReviewPage = 1;
private const int ReviewPageSize = 5;

private int totalReviews => pagedReviews?.TotalCount ?? 0;

protected override async Task OnInitializedAsync()
{
await LoadData();
}
if (ReviewPage.HasValue && ReviewPage.Value > 0)
{
currentReviewPage = ReviewPage.Value;
}

private async Task LoadData()
{
clothingItem = await ClothingItemService.GetByIdAsync(Id);
await LoadClothingItem();
await LoadReviews();

if (clothingItem != null)
{
await LoadReviews();
newReview = new Review { ClothingItemId = Id };
}
}

protected override async Task OnParametersSetAsync()
{
if (ReviewPage.HasValue && ReviewPage.Value > 0 && ReviewPage.Value != currentReviewPage)
{
currentReviewPage = ReviewPage.Value;
await LoadReviews();
}
}

private async Task LoadClothingItem()
{
clothingItem = await ClothingItemService.GetByIdAsync(Id);
}

private async Task LoadReviews()
{
reviews = (await ReviewService.GetReviewsAsync(Id)).ToList();
pagedReviews = await ReviewService.GetReviewsPagedAsync(Id, currentReviewPage, ReviewPageSize);
}

private async Task ChangeReviewPage(int newPage)
{
if (pagedReviews == null)
{
return;
}

if (newPage < 1 || newPage > pagedReviews.TotalPages || newPage == currentReviewPage)
{
return;
}

currentReviewPage = newPage;
Navigation.NavigateTo($"/item/{Id}?reviewPage={currentReviewPage}");
await LoadReviews();
}

private async Task HandleReviewPageKeyDown(KeyboardEventArgs e)
{
if (pagedReviews == null)
{
return;
}

switch (e.Key)
{
case "ArrowLeft":
if (pagedReviews.HasPreviousPage)
{
await ChangeReviewPage(currentReviewPage - 1);
}
break;
case "ArrowRight":
if (pagedReviews.HasNextPage)
{
await ChangeReviewPage(currentReviewPage + 1);
}
break;
case "Home":
if (currentReviewPage != 1)
{
await ChangeReviewPage(1);
}
break;
case "End":
if (currentReviewPage != pagedReviews.TotalPages)
{
await ChangeReviewPage(pagedReviews.TotalPages);
}
break;
}
}

private async Task HandleSubmit()
Expand All @@ -1036,8 +1184,9 @@ else
errorMessage = string.Empty;
StateHasChanged(); // force the render to see the feedback message

// Ricarica i dati del capo
await LoadData();
// Reload clothing item to update average rating, then reload current review page
await LoadClothingItem();
await LoadReviews();

newReview = new Review { ClothingItemId = Id };

Expand Down