From ef40cc0f3b1b3013046820b84e8482f1c6a29533 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 7 Dec 2025 17:44:14 +0900
Subject: feat(client): add deck list page with empty state and list view
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Implement HomePage to display user's decks fetched from the API.
Includes loading state, error handling with retry, and empty state
messaging. Also adds comprehensive tests for the deck list page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/pages/HomePage.tsx | 117 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 115 insertions(+), 2 deletions(-)

(limited to 'src/client/pages/HomePage.tsx')

diff --git a/src/client/pages/HomePage.tsx b/src/client/pages/HomePage.tsx
index 1d65484..c9d0843 100644
--- a/src/client/pages/HomePage.tsx
+++ b/src/client/pages/HomePage.tsx
@@ -1,8 +1,121 @@
+import { useCallback, useEffect, useState } from "react";
+import { ApiClientError, apiClient } from "../api";
+import { useAuth } from "../stores";
+
+interface Deck {
+	id: string;
+	name: string;
+	description: string | null;
+	newCardsPerDay: number;
+	createdAt: string;
+	updatedAt: string;
+}
+
 export function HomePage() {
+	const { logout } = useAuth();
+	const [decks, setDecks] = useState<Deck[]>([]);
+	const [isLoading, setIsLoading] = useState(true);
+	const [error, setError] = useState<string | null>(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]);
+
 	return (
 		<div>
-			<h1>Kioku</h1>
-			<p>Spaced repetition learning app</p>
+			<header
+				style={{
+					display: "flex",
+					justifyContent: "space-between",
+					alignItems: "center",
+					marginBottom: "1rem",
+				}}
+			>
+				<h1>Kioku</h1>
+				<button type="button" onClick={logout}>
+					Logout
+				</button>
+			</header>
+
+			<main>
+				<h2>Your Decks</h2>
+
+				{isLoading && <p>Loading decks...</p>}
+
+				{error && (
+					<div role="alert" style={{ color: "red" }}>
+						{error}
+						<button
+							type="button"
+							onClick={fetchDecks}
+							style={{ marginLeft: "0.5rem" }}
+						>
+							Retry
+						</button>
+					</div>
+				)}
+
+				{!isLoading && !error && decks.length === 0 && (
+					<div>
+						<p>You don't have any decks yet.</p>
+						<p>Create your first deck to start learning!</p>
+					</div>
+				)}
+
+				{!isLoading && !error && decks.length > 0 && (
+					<ul style={{ listStyle: "none", padding: 0 }}>
+						{decks.map((deck) => (
+							<li
+								key={deck.id}
+								style={{
+									border: "1px solid #ccc",
+									padding: "1rem",
+									marginBottom: "0.5rem",
+									borderRadius: "4px",
+								}}
+							>
+								<h3 style={{ margin: 0 }}>{deck.name}</h3>
+								{deck.description && (
+									<p style={{ margin: "0.5rem 0 0 0", color: "#666" }}>
+										{deck.description}
+									</p>
+								)}
+							</li>
+						))}
+					</ul>
+				)}
+			</main>
 		</div>
 	);
 }
-- 
cgit v1.2.3-70-g09d2

