Preview
+ {}
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 8b737e7..54c6e19 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -26,5 +26,5 @@ export async function apiFetch(path: string, options: RequestInit = {}) {
export const api = {
get: (path: string) => apiFetch(path, { method: "GET" }),
- post: (path: string, body: any) => apiFetch(path, { method: "POST", body: JSON.stringify(body) }),
+ post: (path: string, body: unknown) => apiFetch(path, { method: "POST", body: JSON.stringify(body) }),
};
diff --git a/web/src/lib/store.ts b/web/src/lib/store.ts
index 7e77434..9f55f45 100644
--- a/web/src/lib/store.ts
+++ b/web/src/lib/store.ts
@@ -46,6 +46,8 @@ export type Visibility = { type: "Private" } | { type: "Unlisted" } | { type: "P
content: string[];
};
+export type Card = { id?: string; front: string; back: string; mediaUrl?: string };
+
export type Deck = {
id: string;
owner_did: string;
@@ -56,3 +58,11 @@ export type Deck = {
published_at?: string;
fork_of?: string;
};
+
+export type CreateDeckPayload = {
+ title: string;
+ description: string;
+ tags: string[];
+ visibility: Visibility;
+ cards: Card[];
+};
diff --git a/web/src/pages/DeckNew.tsx b/web/src/pages/DeckNew.tsx
index ee830c4..5edd03f 100644
--- a/web/src/pages/DeckNew.tsx
+++ b/web/src/pages/DeckNew.tsx
@@ -2,12 +2,13 @@ import { useNavigate } from "@solidjs/router";
import type { Component } from "solid-js";
import { DeckEditor } from "../components/DeckEditor";
import { api } from "../lib/api";
+import type { Card, CreateDeckPayload } from "../lib/store";
import { toast } from "../lib/toast";
const DeckNew: Component = () => {
const navigate = useNavigate();
- const handleSave = async (data: any) => {
+ const handleSave = async (data: CreateDeckPayload) => {
try {
const { cards, ...deckPayload } = data;
const res = await api.post("/decks", deckPayload);
@@ -17,7 +18,7 @@ const DeckNew: Component = () => {
if (cards && cards.length > 0) {
await Promise.all(
- cards.map((c: any) =>
+ cards.map((c: Card) =>
api.post("/cards", { deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl })
),
);
diff --git a/web/src/pages/DeckView.test.tsx b/web/src/pages/DeckView.test.tsx
new file mode 100644
index 0000000..5583414
--- /dev/null
+++ b/web/src/pages/DeckView.test.tsx
@@ -0,0 +1,67 @@
+import { cleanup, render, screen, waitFor } from "@solidjs/testing-library";
+import { JSX } from "solid-js";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { api } from "../lib/api";
+import DeckView from "./DeckView";
+
+vi.mock("../lib/api", () => ({ api: { get: vi.fn() } }));
+
+vi.mock(
+ "@solidjs/router",
+ () => ({
+ useParams: () => ({ id: "123" }),
+ A: (props: { href: string; children: JSX.Element }) =>
{props.children},
+ }),
+);
+
+describe("DeckView", () => {
+ afterEach(cleanup);
+
+ it("renders deck details and cards", async () => {
+ const deck = {
+ id: "123",
+ title: "Test Deck",
+ description: "A test deck",
+ tags: ["test"],
+ visibility: { type: "Public" },
+ owner_did: "did:test",
+ };
+
+ const cards = [{ id: "c1", front: "Front 1", back: "Back 1" }, { id: "c2", front: "Front 2", back: "Back 2" }];
+
+ vi.mocked(api.get).mockImplementation(
+ ((path: string) => {
+ if (path === "/decks/123") {
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(deck) });
+ }
+ if (path === "/decks/123/cards") {
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(cards) });
+ }
+ return Promise.reject(new Error(`Unexpected path: ${path}`));
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ }) as any,
+ );
+
+ render(() =>
);
+
+ await waitFor(() => expect(screen.getByText("Test Deck")).toBeInTheDocument());
+ expect(screen.getByText("A test deck")).toBeInTheDocument();
+ expect(screen.getByText("#test")).toBeInTheDocument();
+ expect(screen.getByText("Front 1")).toBeInTheDocument();
+ expect(screen.getByText("Front 2")).toBeInTheDocument();
+ expect(screen.getByText("Back 1")).toBeInTheDocument();
+ });
+
+ it("renders not found state when deck returns error", async () => {
+ vi.mocked(api.get).mockImplementation(
+ (() => {
+ return Promise.resolve({ ok: false });
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ }) as any,
+ );
+
+ render(() =>
);
+
+ await waitFor(() => expect(screen.getByText(/Deck not found/i)).toBeInTheDocument());
+ });
+});
diff --git a/web/src/pages/DeckView.tsx b/web/src/pages/DeckView.tsx
new file mode 100644
index 0000000..e4c09df
--- /dev/null
+++ b/web/src/pages/DeckView.tsx
@@ -0,0 +1,124 @@
+import { A, useParams } from "@solidjs/router";
+import type { Component } from "solid-js";
+import { createResource, For, Show } from "solid-js";
+import { api } from "../lib/api";
+import type { Visibility } from "../lib/store";
+
+type Deck = {
+ id: string;
+ title: string;
+ description: string;
+ tags: string[];
+ visibility: Visibility;
+ owner_did: string;
+};
+
+type Card = { id: string; front: string; back?: string };
+
+const fetchDeck = async (id: string): Promise
=> {
+ const res = await api.get(`/decks/${id}`);
+ if (!res.ok) return null;
+ return res.json();
+};
+
+const fetchCards = async (id: string): Promise => {
+ const res = await api.get(`/decks/${id}/cards`);
+ if (!res.ok) return [];
+ return res.json();
+};
+
+const DeckView: Component = () => {
+ const params = useParams();
+
+ const [deck] = createResource(() => params.id, fetchDeck);
+ const [cards] = createResource(() => params.id, fetchCards);
+
+ return (
+
+
+ Loading deck...
+
+
+
+
+ Deck not found or you don't have access.
+
+
+
+
+
+
+
{deck()?.title}
+
+
+ {deck()?.visibility.type}
+
+
+
+
+
{deck()?.description}
+
+
+
+ {(tag) => (
+ #{tag}
+ )}
+
+
+
+
+
+
+
+
+ Cards ({cards()?.length || 0})
+
+
+
+ Loading cards...
+
+
+
+
+ {(card, i) => (
+
+
+ CARD {i() + 1}
+
+
+
+
+
Back
+
{card.back || Empty}
+
+
+
+ )}
+
+
+
+
+ No cards in this deck.
+
+
+
+
+
+
+ );
+};
+
+export default DeckView;
diff --git a/web/src/pages/NotFound.tsx b/web/src/pages/NotFound.tsx
new file mode 100644
index 0000000..04429f4
--- /dev/null
+++ b/web/src/pages/NotFound.tsx
@@ -0,0 +1,21 @@
+import { A } from "@solidjs/router";
+import type { Component } from "solid-js";
+
+const NotFound: Component = () => {
+ return (
+
+
404
+
Page not found
+
+ The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
+
+
+ Go to Library
+
+
+ );
+};
+
+export default NotFound;
diff --git a/web/src/pages/NoteNew.tsx b/web/src/pages/NoteNew.tsx
index 5299344..a9d6ad4 100644
--- a/web/src/pages/NoteNew.tsx
+++ b/web/src/pages/NoteNew.tsx
@@ -1,4 +1,3 @@
-import { useNavigate } from "@solidjs/router";
import { NoteEditor } from "../components/NoteEditor";
const NoteNew = () => {