aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/pages/DeckDetailPage.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-08 00:18:03 +0900
committernsfisis <nsfisis@gmail.com>2025-12-08 00:18:03 +0900
commit65c0adfd769b9ef11b897c96a3634c61120055b8 (patch)
tree74668feef8f134c1b132beaab125e42fa9d77b2e /src/client/pages/DeckDetailPage.tsx
parent7cf55a3b7e37971ea0835118a26f032d895ff71f (diff)
downloadkioku-65c0adfd769b9ef11b897c96a3634c61120055b8.tar.gz
kioku-65c0adfd769b9ef11b897c96a3634c61120055b8.tar.zst
kioku-65c0adfd769b9ef11b897c96a3634c61120055b8.zip
feat(client): redesign frontend with TailwindCSS v4
Replace inline styles with TailwindCSS, implementing a cohesive Japanese-inspired design system with custom colors (cream, teal primary), typography (Fraunces, DM Sans), and animations. Update all pages and components with consistent styling, improve accessibility by adding aria-hidden to decorative SVGs, and configure Biome for Tailwind CSS syntax support. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/client/pages/DeckDetailPage.tsx')
-rw-r--r--src/client/pages/DeckDetailPage.tsx452
1 files changed, 291 insertions, 161 deletions
diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx
index 3d7ffb5..cb1e3fb 100644
--- a/src/client/pages/DeckDetailPage.tsx
+++ b/src/client/pages/DeckDetailPage.tsx
@@ -31,6 +31,13 @@ const CardStateLabels: Record<number, string> = {
3: "Relearning",
};
+const CardStateColors: Record<number, string> = {
+ 0: "bg-info/10 text-info",
+ 1: "bg-warning/10 text-warning",
+ 2: "bg-success/10 text-success",
+ 3: "bg-error/10 text-error",
+};
+
export function DeckDetailPage() {
const { deckId } = useParams<{ deckId: string }>();
const [deck, setDeck] = useState<Deck | null>(null);
@@ -114,195 +121,318 @@ export function DeckDetailPage() {
if (!deckId) {
return (
- <div>
- <p>Invalid deck ID</p>
- <Link href="/">Back to decks</Link>
+ <div className="min-h-screen bg-cream flex items-center justify-center">
+ <div className="text-center">
+ <p className="text-muted mb-4">Invalid deck ID</p>
+ <Link
+ href="/"
+ className="text-primary hover:text-primary-dark font-medium"
+ >
+ Back to decks
+ </Link>
+ </div>
</div>
);
}
return (
- <div>
- <header style={{ marginBottom: "1rem" }}>
- <Link href="/" style={{ textDecoration: "none" }}>
- &larr; Back to Decks
- </Link>
- </header>
-
- {isLoading && <p>Loading...</p>}
-
- {error && (
- <div role="alert" style={{ color: "red" }}>
- {error}
- <button
- type="button"
- onClick={fetchData}
- style={{ marginLeft: "0.5rem" }}
+ <div className="min-h-screen bg-cream">
+ {/* Header */}
+ <header className="bg-white border-b border-border/50">
+ <div className="max-w-4xl mx-auto px-4 py-4">
+ <Link
+ href="/"
+ className="inline-flex items-center gap-2 text-muted hover:text-slate transition-colors text-sm"
>
- Retry
- </button>
+ <svg
+ className="w-4 h-4"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M15 19l-7-7 7-7"
+ />
+ </svg>
+ Back to Decks
+ </Link>
</div>
- )}
+ </header>
- {!isLoading && !error && deck && (
- <main>
- <div style={{ marginBottom: "1.5rem" }}>
- <h1 style={{ margin: 0 }}>{deck.name}</h1>
- {deck.description && (
- <p style={{ margin: "0.5rem 0 0 0", color: "#666" }}>
- {deck.description}
- </p>
- )}
+ {/* Main Content */}
+ <main className="max-w-4xl mx-auto px-4 py-8">
+ {/* Loading State */}
+ {isLoading && (
+ <div className="flex items-center justify-center py-12">
+ <svg
+ className="animate-spin h-8 w-8 text-primary"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <circle
+ className="opacity-25"
+ cx="12"
+ cy="12"
+ r="10"
+ stroke="currentColor"
+ strokeWidth="4"
+ fill="none"
+ />
+ <path
+ className="opacity-75"
+ fill="currentColor"
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
+ />
+ </svg>
</div>
+ )}
+ {/* Error State */}
+ {error && (
<div
- style={{
- display: "flex",
- gap: "0.5rem",
- marginBottom: "1rem",
- }}
+ role="alert"
+ className="bg-error/5 border border-error/20 rounded-xl p-4 flex items-center justify-between"
>
- <Link href={`/decks/${deckId}/study`}>
+ <span className="text-error">{error}</span>
+ <button
+ type="button"
+ onClick={fetchData}
+ className="text-error hover:text-error/80 font-medium text-sm"
+ >
+ Retry
+ </button>
+ </div>
+ )}
+
+ {/* Deck Content */}
+ {!isLoading && !error && deck && (
+ <div className="animate-fade-in">
+ {/* Deck Header */}
+ <div className="mb-8">
+ <h1 className="font-display text-3xl font-semibold text-ink mb-2">
+ {deck.name}
+ </h1>
+ {deck.description && (
+ <p className="text-muted">{deck.description}</p>
+ )}
+ </div>
+
+ {/* Study Button */}
+ <div className="mb-8">
+ <Link
+ href={`/decks/${deckId}/study`}
+ className="inline-flex items-center gap-2 bg-success hover:bg-success/90 text-white font-medium py-3 px-6 rounded-xl transition-all duration-200 active:scale-[0.98] shadow-sm hover:shadow-md"
+ >
+ <svg
+ className="w-5 h-5"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
+ />
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+ />
+ </svg>
+ Study Now
+ </Link>
+ </div>
+
+ {/* Cards Section */}
+ <div className="flex items-center justify-between mb-6">
+ <h2 className="font-display text-xl font-medium text-slate">
+ Cards{" "}
+ <span className="text-muted font-normal">({cards.length})</span>
+ </h2>
<button
type="button"
- style={{
- backgroundColor: "#28a745",
- color: "white",
- border: "none",
- padding: "0.5rem 1rem",
- borderRadius: "4px",
- cursor: "pointer",
- }}
+ onClick={() => setIsCreateModalOpen(true)}
+ className="inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 active:scale-[0.98]"
>
- Study Now
+ <svg
+ className="w-5 h-5"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M12 4v16m8-8H4"
+ />
+ </svg>
+ Add Card
</button>
- </Link>
- </div>
-
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- marginBottom: "1rem",
- }}
- >
- <h2 style={{ margin: 0 }}>Cards ({cards.length})</h2>
- <button type="button" onClick={() => setIsCreateModalOpen(true)}>
- Add Card
- </button>
- </div>
-
- {cards.length === 0 && (
- <div>
- <p>This deck has no cards yet.</p>
- <p>Add cards to start studying!</p>
</div>
- )}
-
- {cards.length > 0 && (
- <ul style={{ listStyle: "none", padding: 0 }}>
- {cards.map((card) => (
- <li
- key={card.id}
- style={{
- border: "1px solid #ccc",
- padding: "1rem",
- marginBottom: "0.5rem",
- borderRadius: "4px",
- }}
+
+ {/* Empty State */}
+ {cards.length === 0 && (
+ <div className="text-center py-12 bg-white rounded-xl border border-border/50">
+ <div className="w-14 h-14 mx-auto mb-4 bg-ivory rounded-xl flex items-center justify-center">
+ <svg
+ className="w-7 h-7 text-muted"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={1.5}
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
+ />
+ </svg>
+ </div>
+ <h3 className="font-display text-lg font-medium text-slate mb-2">
+ No cards yet
+ </h3>
+ <p className="text-muted text-sm mb-4">
+ Add cards to start studying
+ </p>
+ <button
+ type="button"
+ onClick={() => setIsCreateModalOpen(true)}
+ className="inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-all duration-200"
>
+ <svg
+ className="w-5 h-5"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M12 4v16m8-8H4"
+ />
+ </svg>
+ Add Your First Card
+ </button>
+ </div>
+ )}
+
+ {/* Card List */}
+ {cards.length > 0 && (
+ <div className="space-y-3">
+ {cards.map((card, index) => (
<div
- style={{
- display: "flex",
- justifyContent: "space-between",
- alignItems: "flex-start",
- }}
+ key={card.id}
+ className="bg-white rounded-xl border border-border/50 p-5 shadow-card hover:shadow-md transition-all duration-200"
+ style={{ animationDelay: `${index * 30}ms` }}
>
- <div style={{ flex: 1, minWidth: 0 }}>
- <div
- style={{
- display: "flex",
- gap: "1rem",
- marginBottom: "0.5rem",
- }}
- >
- <div style={{ flex: 1, minWidth: 0 }}>
- <strong>Front:</strong>
- <p
- style={{
- margin: "0.25rem 0 0 0",
- whiteSpace: "pre-wrap",
- wordBreak: "break-word",
- }}
- >
- {card.front}
- </p>
+ <div className="flex items-start justify-between gap-4">
+ <div className="flex-1 min-w-0">
+ {/* Front/Back Preview */}
+ <div className="grid grid-cols-2 gap-4 mb-3">
+ <div>
+ <span className="text-xs font-medium text-muted uppercase tracking-wide">
+ Front
+ </span>
+ <p className="mt-1 text-slate text-sm line-clamp-2 whitespace-pre-wrap break-words">
+ {card.front}
+ </p>
+ </div>
+ <div>
+ <span className="text-xs font-medium text-muted uppercase tracking-wide">
+ Back
+ </span>
+ <p className="mt-1 text-slate text-sm line-clamp-2 whitespace-pre-wrap break-words">
+ {card.back}
+ </p>
+ </div>
</div>
- <div style={{ flex: 1, minWidth: 0 }}>
- <strong>Back:</strong>
- <p
- style={{
- margin: "0.25rem 0 0 0",
- whiteSpace: "pre-wrap",
- wordBreak: "break-word",
- }}
+
+ {/* Card Stats */}
+ <div className="flex items-center gap-3 text-xs">
+ <span
+ className={`px-2 py-0.5 rounded-full font-medium ${CardStateColors[card.state] || "bg-muted/10 text-muted"}`}
>
- {card.back}
- </p>
+ {CardStateLabels[card.state] || "Unknown"}
+ </span>
+ <span className="text-muted">
+ {card.reps} reviews
+ </span>
+ {card.lapses > 0 && (
+ <span className="text-muted">
+ {card.lapses} lapses
+ </span>
+ )}
</div>
</div>
- <div
- style={{
- display: "flex",
- gap: "1rem",
- fontSize: "0.875rem",
- color: "#666",
- }}
- >
- <span>
- State: {CardStateLabels[card.state] || "Unknown"}
- </span>
- <span>Reviews: {card.reps}</span>
- <span>Lapses: {card.lapses}</span>
+
+ {/* Actions */}
+ <div className="flex items-center gap-1 shrink-0">
+ <button
+ type="button"
+ onClick={() => setEditingCard(card)}
+ className="p-2 text-muted hover:text-slate hover:bg-ivory rounded-lg transition-colors"
+ title="Edit card"
+ >
+ <svg
+ className="w-4 h-4"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
+ />
+ </svg>
+ </button>
+ <button
+ type="button"
+ onClick={() => setDeletingCard(card)}
+ className="p-2 text-muted hover:text-error hover:bg-error/5 rounded-lg transition-colors"
+ title="Delete card"
+ >
+ <svg
+ className="w-4 h-4"
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
+ />
+ </svg>
+ </button>
</div>
</div>
- <div
- style={{
- display: "flex",
- gap: "0.5rem",
- marginLeft: "1rem",
- }}
- >
- <button
- type="button"
- onClick={() => setEditingCard(card)}
- >
- Edit
- </button>
- <button
- type="button"
- onClick={() => setDeletingCard(card)}
- style={{
- backgroundColor: "#dc3545",
- color: "white",
- border: "none",
- padding: "0.5rem 1rem",
- borderRadius: "4px",
- cursor: "pointer",
- }}
- >
- Delete
- </button>
- </div>
</div>
- </li>
- ))}
- </ul>
- )}
- </main>
- )}
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ </main>
+ {/* Modals */}
{deckId && (
<CreateCardModal
isOpen={isCreateModalOpen}