From f8e4be9b36a16969ac53bd9ce12ce8064be10196 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Jan 2026 17:43:59 +0900 Subject: refactor(client): migrate state management from React Context to Jotai MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace AuthProvider and SyncProvider with Jotai atoms for more granular state management and better performance. This migration: - Creates atoms for auth, sync, decks, cards, noteTypes, and study state - Uses atomFamily for parameterized state (e.g., cards by deckId) - Introduces StoreInitializer component for subscription initialization - Updates all components and pages to use useAtomValue/useSetAtom - Updates all tests to use Jotai Provider with createStore pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/client/pages/HomePage.tsx | 265 ++++++++++++++++++------------------------ 1 file changed, 110 insertions(+), 155 deletions(-) (limited to 'src/client/pages/HomePage.tsx') diff --git a/src/client/pages/HomePage.tsx b/src/client/pages/HomePage.tsx index ddf97e2..e0e9e9e 100644 --- a/src/client/pages/HomePage.tsx +++ b/src/client/pages/HomePage.tsx @@ -3,72 +3,121 @@ import { faLayerGroup, faPen, faPlus, - faSpinner, faTrash, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useCallback, useEffect, useState } from "react"; +import { useAtomValue, useSetAtom } from "jotai"; +import { Suspense, useState, useTransition } from "react"; import { Link } from "wouter"; -import { ApiClientError, apiClient } from "../api"; +import { type Deck, decksAtom, logoutAtom } from "../atoms"; import { CreateDeckModal } from "../components/CreateDeckModal"; import { DeleteDeckModal } from "../components/DeleteDeckModal"; import { EditDeckModal } from "../components/EditDeckModal"; +import { ErrorBoundary } from "../components/ErrorBoundary"; +import { LoadingSpinner } from "../components/LoadingSpinner"; import { SyncButton } from "../components/SyncButton"; import { SyncStatusIndicator } from "../components/SyncStatusIndicator"; -import { useAuth } from "../stores"; -interface Deck { - id: string; - name: string; - description: string | null; - newCardsPerDay: number; - createdAt: string; - updatedAt: string; +function DeckList({ + onEditDeck, + onDeleteDeck, +}: { + onEditDeck: (deck: Deck) => void; + onDeleteDeck: (deck: Deck) => void; +}) { + const decks = useAtomValue(decksAtom); + + if (decks.length === 0) { + return ( +
+
+
+

+ No decks yet +

+

+ Create your first deck to start learning +

+
+ ); + } + + return ( +
+ {decks.map((deck, index) => ( +
+
+
+ +

+ {deck.name} +

+ + {deck.description && ( +

+ {deck.description} +

+ )} +
+
+ + +
+
+
+ ))} +
+ ); } export function HomePage() { - const { logout } = useAuth(); - const [decks, setDecks] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const logout = useSetAtom(logoutAtom); + const reloadDecks = useSetAtom(decksAtom); + const [, startTransition] = useTransition(); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [editingDeck, setEditingDeck] = useState(null); const [deletingDeck, setDeletingDeck] = useState(null); - const fetchDecks = useCallback(async () => { - setIsLoading(true); - setError(null); - - try { - const res = await apiClient.rpc.api.decks.$get(undefined, { - headers: apiClient.getAuthHeader(), - }); - - if (!res.ok) { - const errorBody = await res.json().catch(() => ({})); - throw new ApiClientError( - (errorBody as { error?: string }).error || - `Request failed with status ${res.status}`, - res.status, - ); - } - - const data = await res.json(); - setDecks(data.decks); - } catch (err) { - if (err instanceof ApiClientError) { - setError(err.message); - } else { - setError("Failed to load decks. Please try again."); - } - } finally { - setIsLoading(false); - } - }, []); - - useEffect(() => { - fetchDecks(); - }, [fetchDecks]); + const handleDeckMutation = () => { + startTransition(() => { + reloadDecks(); + }); + }; return (
@@ -95,7 +144,7 @@ export function HomePage() {
- {/* Loading State */} - {isLoading && ( -
-
- )} - - {/* Error State */} - {error && ( -
- {error} - -
- )} - - {/* Empty State */} - {!isLoading && !error && decks.length === 0 && ( -
-
-
-

- No decks yet -

-

- Create your first deck to start learning -

-
- )} - - {/* Deck List */} - {!isLoading && !error && decks.length > 0 && ( -
- {decks.map((deck, index) => ( -
-
-
- -

- {deck.name} -

- - {deck.description && ( -

- {deck.description} -

- )} -
-
- - -
-
-
- ))} -
- )} + + {/* Modals */} setIsCreateModalOpen(false)} - onDeckCreated={fetchDecks} + onDeckCreated={handleDeckMutation} /> setEditingDeck(null)} - onDeckUpdated={fetchDecks} + onDeckUpdated={handleDeckMutation} /> setDeletingDeck(null)} - onDeckDeleted={fetchDecks} + onDeckDeleted={handleDeckMutation} /> ); -- cgit v1.2.3-70-g09d2