|
| 1 | +# New Landing / Home Page |
| 2 | + |
| 3 | +**Covers:** Issue [#214](https://github.com/xenobiasoft/sudoku/issues/214) |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## 1. 🧭 Overview |
| 8 | + |
| 9 | +**Feature Name:** New Landing / Home Page |
| 10 | + |
| 11 | +**Problem Statement:** |
| 12 | +The current landing page (`/`) uses an accordion-style menu to expose "Start New Game" and "Load Game" actions. This buries available features behind toggled sub-menus, offers no visual distinction between a new visitor and a returning player, and immediately redirects brand-new users to the profile-creation flow before they can see what the app offers. The result is a confusing first impression with no clear call-to-action hierarchy. |
| 13 | + |
| 14 | +**Goals:** |
| 15 | +- Replace the accordion menu with a card-based home screen presenting three explicit navigation cards |
| 16 | +- Dynamically show "Create Profile" vs. "Manage Profile" based on whether the player has a saved local profile |
| 17 | +- Disable "Start New Game" and "Browse Game List" for players who have not yet created a profile, with visual affordance explaining why |
| 18 | +- Introduce a dedicated **Select Difficulty** page (replaces the inline difficulty accordion) |
| 19 | +- Introduce a dedicated **Game List** page (replaces the inline saved-game accordion) |
| 20 | +- Implement feature parity across both Blazor Server and React/Vite frontends |
| 21 | + |
| 22 | +**Non-Goals:** |
| 23 | +- Redesigning the difficulty selection page beyond a simple three-option picker |
| 24 | +- Adding filtering, sorting, or pagination to the Game List page |
| 25 | +- Changing the profile creation or profile management flows |
| 26 | +- Adding an onboarding wizard, tutorial, or guided tour |
| 27 | +- Any backend, API, or domain changes |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## 2. 📌 Functional Requirements |
| 32 | + |
| 33 | +| ID | Requirement | |
| 34 | +|----|-------------| |
| 35 | +| FR-1 | The home page at `/` shall display three navigation cards: **Profile** (Create / Manage), **Start New Game**, and **Browse Game List**. | |
| 36 | +| FR-2 | The **Profile** card shall show the label "Create Profile" and navigate to `/create-profile` when no local profile exists, and the label "Manage Profile" navigating to `/profile` when a profile is stored. | |
| 37 | +| FR-3 | The **Start New Game** card shall be visually disabled (non-interactive) when no local player profile exists. | |
| 38 | +| FR-4 | The **Browse Game List** card shall be visually disabled (non-interactive) when no local player profile exists. | |
| 39 | +| FR-5 | Clicking **Start New Game** (when enabled) shall navigate to a new **Select Difficulty** page. | |
| 40 | +| FR-6 | The **Select Difficulty** page shall present Easy, Medium, and Hard options. Selecting one shall navigate to the existing game-creation route (`/new/{difficulty}`). | |
| 41 | +| FR-7 | Clicking **Browse Game List** (when enabled) shall navigate to a new **Game List** page. | |
| 42 | +| FR-8 | The **Game List** page shall display all in-progress saved games for the current player, with the ability to load (navigate to `/game/{id}`) or delete each game, and an empty-state message when no games exist. | |
| 43 | +| FR-9 | The home page shall **not** automatically redirect new players to `/create-profile`; the "Create Profile" card is the explicit call-to-action. | |
| 44 | +| FR-10 | The home page shall determine player state (new vs. returning) from local storage only, with no backend API call on initial render. | |
| 45 | +| FR-11 | Both Blazor Server and React/Vite frontends shall implement the above with feature parity. | |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## 3. 🛡️ Non-Functional Requirements |
| 50 | + |
| 51 | +- **Performance:** The home page must render without a full-page loading spinner for returning players. The player-state check must read local storage only and must not block the initial paint. |
| 52 | +- **Accessibility:** Navigation cards must be keyboard-accessible (focusable, activated by Enter/Space). Disabled cards must use `disabled` attribute or `aria-disabled="true"` with visible helper text explaining the requirement to create a profile. |
| 53 | +- **Reliability:** A local-storage read failure shall silently treat the user as a new player (all game-action cards disabled; no crash). |
| 54 | +- **Observability:** Blazor: log page initialization and navigation events using the existing `ILogger<T>` pattern. React: no change to current logging behaviour. |
| 55 | +- **Deployment:** Frontend-only change; no infrastructure, deployment pipeline, or backend configuration changes required. |
| 56 | +- **Scalability:** Not applicable (pure UI change). |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## 4. 🏛️ Architecture Overview |
| 61 | + |
| 62 | +**High-Level Description:** |
| 63 | +This is a pure UI-layer change across both frontends. No new backend endpoints, CQRS commands/queries, domain aggregates, or infrastructure components are required. The home page reads the locally-stored `ProfileInfo` object to decide card state; all other data fetching is deferred to the new dedicated pages. |
| 64 | + |
| 65 | +**Affected Projects:** |
| 66 | +- `src/frontend/Sudoku.Blazor` — `Index.razor` / `Index.razor.cs` (replaced), new `SelectDifficulty.razor`, new `GameList.razor`, routing |
| 67 | +- `src/frontend/Sudoku.React` — `HomePage.tsx` (replaced), new `SelectDifficultyPage.tsx`, new `GameListPage.tsx`, `App.tsx` routing, `usePlayerService` hook |
| 68 | + |
| 69 | +**Sequence Diagram:** |
| 70 | + |
| 71 | +```mermaid |
| 72 | +sequenceDiagram |
| 73 | + participant User |
| 74 | + participant HomePage |
| 75 | + participant LocalStorage |
| 76 | + participant SelectDifficultyPage |
| 77 | + participant NewGamePage |
| 78 | + participant GameListPage |
| 79 | +
|
| 80 | + User->>HomePage: Navigate to / |
| 81 | + HomePage->>LocalStorage: Read ProfileInfo (sync / async) |
| 82 | + LocalStorage-->>HomePage: ProfileInfo | null |
| 83 | +
|
| 84 | + alt New Player (no profile) |
| 85 | + HomePage-->>User: Cards rendered — "Create Profile" enabled, others disabled |
| 86 | + User->>HomePage: Click "Create Profile" |
| 87 | + HomePage->>User: Navigate to /create-profile |
| 88 | + else Returning Player |
| 89 | + HomePage-->>User: All three cards enabled |
| 90 | + User->>HomePage: Click "Start New Game" |
| 91 | + HomePage->>SelectDifficultyPage: Navigate to /select-difficulty |
| 92 | + User->>SelectDifficultyPage: Choose difficulty |
| 93 | + SelectDifficultyPage->>NewGamePage: Navigate to /new/{difficulty} |
| 94 | + NewGamePage->>User: Create game → navigate to /game/{id} |
| 95 | + end |
| 96 | +
|
| 97 | + User->>HomePage: Click "Browse Game List" |
| 98 | + HomePage->>GameListPage: Navigate to /games |
| 99 | + GameListPage->>User: Fetch and display saved games |
| 100 | +``` |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +## 5. 📦 Data Models & Contracts |
| 105 | + |
| 106 | +**Domain Models:** No changes. |
| 107 | + |
| 108 | +**DTOs / API Contracts:** No changes. The existing `GameModel` DTO (returned by `GET /api/games/{alias}`) is reused on the Game List page. |
| 109 | + |
| 110 | +**Persistence Changes:** None. Local storage keys (`sudoku-profile`, `sudoku-alias`) and their shapes are unchanged. |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## 6. 🔄 CQRS Components |
| 115 | + |
| 116 | +No new CQRS components. The existing `GetPlayerGamesQuery` (used by `GameManager.LoadGamesAsync` in Blazor and `useGameService.loadGames` in React) is reused without modification on the Game List page. |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## 7. 📣 Domain Events |
| 121 | + |
| 122 | +Not applicable. No domain events are raised or consumed by these UI pages. |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## 8. 🖥️ UI/UX Flow |
| 127 | + |
| 128 | +**Frontend Target:** Both Blazor Server and React/Vite |
| 129 | + |
| 130 | +### Home Page (`/`) |
| 131 | + |
| 132 | +Three navigation cards arranged in a grid or column layout. Each card has an icon, a title, and an optional subtitle. |
| 133 | + |
| 134 | +| Card | New Player | Returning Player | Destination | |
| 135 | +|------|-----------|-----------------|-------------| |
| 136 | +| Profile | "Create Profile" — **enabled** | "Manage Profile" — **enabled** | `/create-profile` or `/profile` | |
| 137 | +| Start New Game | **disabled** + helper text | **enabled** | `/select-difficulty` | |
| 138 | +| Browse Game List | **disabled** + helper text | **enabled** | `/games` | |
| 139 | + |
| 140 | +- No loading spinner on initial page render for returning players. |
| 141 | +- Helper text on disabled cards (e.g., *"Create a profile to unlock this"*) guides new users toward the Profile card. |
| 142 | + |
| 143 | +### Select Difficulty Page (`/select-difficulty`) |
| 144 | + |
| 145 | +- Minimal page with three option buttons: Easy, Medium, Hard. |
| 146 | +- Selecting a difficulty navigates to `/new/{difficulty}` (the existing game-creation route). |
| 147 | +- Includes back navigation to `/`. |
| 148 | +- Requires an active player profile (redirect to `/` if no profile found). |
| 149 | + |
| 150 | +### Game List Page (`/games`) |
| 151 | + |
| 152 | +- Fetches and displays all in-progress saved games for the current player using the existing `GameThumbnail` / game-card components. |
| 153 | +- Load action navigates to `/game/{id}`. |
| 154 | +- Delete action removes the game and refreshes the list in place. |
| 155 | +- Empty state: displays a message such as *"No saved games yet. Start a new game to get going!"* |
| 156 | +- Error state: displays an error message if the API call fails. |
| 157 | +- Includes back navigation to `/`. |
| 158 | +- Requires an active player profile (redirect to `/` if no profile found). |
| 159 | + |
| 160 | +**State Management:** |
| 161 | +- **Home page:** reads local storage on mount; no server calls; no loading state required. |
| 162 | +- **Select Difficulty page:** stateless; navigation only. |
| 163 | +- **Game List page:** fetches saved games via existing game service/hook on mount; manages loading and error states locally. |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## 9. 🌐 API Endpoints |
| 168 | + |
| 169 | +No new API endpoints. The existing `GET /api/games/{alias}` endpoint is used by the Game List page. |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +## 10. 🧪 Testing Strategy |
| 174 | + |
| 175 | +**Unit Tests — React (Vitest + React Testing Library):** |
| 176 | +- `HomePage`: renders three cards; correct enabled/disabled state based on mocked player service; navigation targets; no API call on mount. |
| 177 | +- `SelectDifficultyPage`: renders three difficulty buttons; clicking each navigates to the correct `/new/{difficulty}` route. |
| 178 | +- `GameListPage`: renders game list from mocked service; load navigates to `/game/{id}`; delete calls service and removes item; empty state; error state. |
| 179 | + |
| 180 | +**Unit Tests — Blazor (bunit):** |
| 181 | +- `Index` page: renders three cards for a new player with only "Create Profile" enabled; renders all cards enabled for a returning player; no backend call on `OnAfterRenderAsync`. |
| 182 | +- `SelectDifficulty` page: renders three difficulty buttons; clicking each triggers correct navigation. |
| 183 | +- `GameList` page: renders game thumbnails; load and delete actions behave correctly. |
| 184 | + |
| 185 | +**Integration Tests:** No new integration tests required. Existing API-layer tests cover game retrieval and deletion. |
| 186 | + |
| 187 | +**Test Data / Fixtures:** |
| 188 | +- React: reuse the existing `makeGame()` helper from `src/frontend/Sudoku.React/src/test/helpers`. |
| 189 | +- Blazor: use existing mock service patterns. |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## 11. ⚠️ Risks & Considerations |
| 194 | + |
| 195 | +- **Behaviour change — new-player onboarding:** The current flow auto-redirects unauthenticated users to `/create-profile`. The new flow presents a home screen instead. The "Create Profile" card must be visually prominent enough (e.g., featured/highlighted card) to drive new users to complete onboarding before trying to play. |
| 196 | +- **`usePlayerService` auto-navigation (React):** The hook currently calls `navigate('/create-profile')` whenever no profile is detected. This side effect must be removed or conditionally bypassed for the home page to render correctly for new players. A read-only local-storage utility or a new `isNewPlayer` flag exposed by the hook may be needed. |
| 197 | +- **Blazor `EnsureProfileInitializedAsync` backend call:** The current `Index.razor.cs` calls `EnsureProfileInitializedAsync()`, which hits the backend API. This call should be removed from the home-page initialization so the page renders without a network round-trip. Backend profile validation can be deferred to when the player attempts to start or load a game. |
| 198 | +- **Existing tests:** `HomePage.test.tsx` (React) and the Blazor `Index` page tests will need to be updated or replaced to reflect the new card-based UI. Difficulty and game-list tests will move to their respective new page test files. |
| 199 | +- **Route additions:** Two new routes (`/select-difficulty` and `/games`) must be registered in `App.tsx` (React) and the Blazor `Routes.razor` / `App.razor`. |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## 12. 🔧 Implementation Plan |
| 204 | + |
| 205 | +1. **React frontend** |
| 206 | + - Refactor `usePlayerService` to expose an `isNewPlayer` boolean derived from local storage only, removing the auto-navigate-to-create-profile side effect from the home page context. |
| 207 | + - Replace `HomePage.tsx` with a card-based layout; update `HomePage.module.css`. |
| 208 | + - Create `SelectDifficultyPage.tsx` at `/select-difficulty`. |
| 209 | + - Create `GameListPage.tsx` at `/games`, extracting the saved-game list logic previously inline in `HomePage.tsx`. |
| 210 | + - Register the two new routes in `App.tsx`. |
| 211 | + - Update `HomePage.test.tsx`; add `SelectDifficultyPage.test.tsx` and `GameListPage.test.tsx`. |
| 212 | + |
| 213 | +2. **Blazor frontend** |
| 214 | + - Rewrite `Index.razor` and `Index.razor.cs` with card-based layout; remove `EnsureProfileInitializedAsync` call on initial render; use local-storage-only profile check. |
| 215 | + - Create `SelectDifficulty.razor` at `/select-difficulty`. |
| 216 | + - Create `GameList.razor` at `/games`, extracting game-list logic from the current `Index.razor.cs`. |
| 217 | + - Update bunit tests for all three pages. |
| 218 | + |
| 219 | +--- |
| 220 | + |
| 221 | +## 13. ❓ Open Questions |
| 222 | + |
| 223 | +1. **Route slugs:** Should the new pages live at `/select-difficulty` and `/games`, or are other slugs preferred (e.g., `/difficulty`, `/game-list`, `/my-games`)? |
| 224 | +2. **Card visual design:** Should the cards reuse the existing Font Awesome icons (`fa-play-circle`, `fa-folder-open`, `fa-user`) or adopt a new visual treatment? |
| 225 | +3. **Disabled-card helper text:** What exact copy should appear on disabled cards? Options include *"Create a profile to unlock this"*, *"Profile required"*, or simply leaving the card greyed out with no additional text. |
| 226 | +4. **Featured / highlighted card for new players:** Should the "Create Profile" card be visually differentiated (e.g., accent border, larger, or positioned first) when the player has no profile, to ensure discoverability? |
| 227 | +5. **Back navigation:** Should the Select Difficulty and Game List pages include an explicit "Back to Home" button, or rely on the browser back button / a shared nav component? |
| 228 | +6. **Profile validation deferral:** When a returning player clicks "Start New Game" or "Browse Game List", should the app perform a silent backend profile check at that point (to catch stale/orphaned profiles) before proceeding, or should it wait until the game action itself fails? |
0 commit comments