From 1a9150f090ddcc7ac67cd9376be07e4dca08f9e8 Mon Sep 17 00:00:00 2001 From: David Ortinau Date: Thu, 30 Apr 2026 17:57:57 -0500 Subject: [PATCH] feat(dashboard): make vocabulary tiles tappable to filtered Vocabulary list The 5 vocab buckets on the Dashboard (New/Learning/Familiar/Review/Known) now navigate to /vocabulary?q=status:{bucket} when tapped. Counts on the tiles match the filtered Vocabulary page exactly. Changes: - Index.razor: wrap each tile in with .vocab-tile-hover affordance - Vocabulary.razor: add "new" and "review" cases to the status filter switch (mirror the bucketing logic in VocabularyProgressRepository GetVocabSummaryCountsAsync exactly); add both to the desktop & mobile dropdown shortcuts - app.css: .vocab-tile-hover hover/active styles E2E validation: Jayne ran a DB-level cross-check; all 5 bucket counts match between Dashboard and filtered Vocabulary list (New=1891, Learning=49, Familiar=282, Review=279, Known=88 for the test user). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SentenceStudio.UI/Pages/Index.razor | 50 ++++++++++++-------- src/SentenceStudio.UI/Pages/Vocabulary.razor | 24 ++++++++-- src/SentenceStudio.UI/wwwroot/css/app.css | 15 ++++++ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/SentenceStudio.UI/Pages/Index.razor b/src/SentenceStudio.UI/Pages/Index.razor index b516afa8..ada59ce5 100644 --- a/src/SentenceStudio.UI/Pages/Index.razor +++ b/src/SentenceStudio.UI/Pages/Index.razor @@ -472,34 +472,44 @@ else if (vocabSummary is not null) var total = vocabSummary.New + vocabSummary.Learning + vocabSummary.Familiar + vocabSummary.Review + vocabSummary.Known;
-
-
@vocabSummary.Learning
-
@Localize["Dashboard_VocabLearning"]
-
+ +
+
@vocabSummary.Learning
+
@Localize["Dashboard_VocabLearning"]
+
+
-
-
@vocabSummary.Familiar
-
@Localize["Dashboard_VocabFamiliar"]
-
+ +
+
@vocabSummary.Familiar
+
@Localize["Dashboard_VocabFamiliar"]
+
+
-
-
@vocabSummary.Review
-
@Localize["Dashboard_VocabReview"]
-
+ +
+
@vocabSummary.Review
+
@Localize["Dashboard_VocabReview"]
+
+
-
-
@vocabSummary.Known
-
@Localize["Dashboard_VocabKnown"]
-
+ +
+
@vocabSummary.Known
+
@Localize["Dashboard_VocabKnown"]
+
+
diff --git a/src/SentenceStudio.UI/Pages/Vocabulary.razor b/src/SentenceStudio.UI/Pages/Vocabulary.razor index bbb6604c..e78c181a 100644 --- a/src/SentenceStudio.UI/Pages/Vocabulary.razor +++ b/src/SentenceStudio.UI/Pages/Vocabulary.razor @@ -158,9 +158,11 @@ else @@ -263,9 +265,11 @@ else @@ -881,6 +885,18 @@ else "familiar" => filtered.Where(v => v.StatusText == "Familiar"), "learning" => filtered.Where(v => v.StatusText == "Learning"), "unknown" => filtered.Where(v => v.StatusText == "Unknown"), + // "new" = words never practiced (no progress OR progress with 0 attempts, excluding Familiar) + "new" => filtered.Where(v => + v.Progress == null + || (v.Progress.TotalAttempts == 0 + && !(v.Progress.IsUserDeclared && v.Progress.VerificationState == VerificationStatus.Pending))), + // "review" = words past their NextReviewDate (not Familiar, not Known) + "review" => filtered.Where(v => + v.Progress != null + && !(v.Progress.IsUserDeclared && v.Progress.VerificationState == VerificationStatus.Pending) + && !v.Progress.IsKnown + && v.Progress.NextReviewDate != null + && v.Progress.NextReviewDate <= DateTime.Now), _ => filtered }, "tag" => filtered.Where(v => diff --git a/src/SentenceStudio.UI/wwwroot/css/app.css b/src/SentenceStudio.UI/wwwroot/css/app.css index 9487e7b9..f0529d1a 100644 --- a/src/SentenceStudio.UI/wwwroot/css/app.css +++ b/src/SentenceStudio.UI/wwwroot/css/app.css @@ -1785,3 +1785,18 @@ main.main-content:has(.matching-page-wrapper) { .activity-dot-complete { box-shadow: 0 0 0 2px var(--bs-success); } + +/* Vocabulary tile hover effect */ +.vocab-tile-hover { + cursor: pointer; + transition: transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.vocab-tile-hover:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.vocab-tile-hover:active { + transform: translateY(0); +}