diff --git a/web/package.json b/web/package.json index 8852d83..2a522b1 100644 --- a/web/package.json +++ b/web/package.json @@ -12,8 +12,8 @@ "lint": "eslint ." }, "dependencies": { - "@fontsource-variable/alegreya": "^5.2.8", - "@fontsource-variable/lora": "^5.2.8", + "@fontsource-variable/figtree": "^5.2.8", + "@fontsource-variable/source-serif-4": "^5.2.8", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.4", "@tailwindcss/vite": "^4.1.18", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 047965c..1cb60ae 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,12 +11,12 @@ importers: .: dependencies: - '@fontsource-variable/alegreya': + '@fontsource-variable/figtree': specifier: ^5.2.8 - version: 5.2.8 - '@fontsource-variable/lora': + version: 5.2.10 + '@fontsource-variable/source-serif-4': specifier: ^5.2.8 - version: 5.2.8 + version: 5.2.9 '@solidjs/meta': specifier: ^0.29.4 version: 0.29.4(solid-js@1.9.10) @@ -316,11 +316,11 @@ packages: '@exodus/crypto': optional: true - '@fontsource-variable/alegreya@5.2.8': - resolution: {integrity: sha512-gQcIA7j76KYTOcdkfo1Xee9xLBi5mya4qTkzlgeoHf9SjOL/gJj5GSSOg/7ba/ciUU18K92i7VGxXzFhDsowGg==} + '@fontsource-variable/figtree@5.2.10': + resolution: {integrity: sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA==} - '@fontsource-variable/lora@5.2.8': - resolution: {integrity: sha512-cxjTJ9BbOWIzusewR4UMBLVePvTSWV6dtNaNsCkF/oKoyA68fJGWfaYCILOOP1BObE4dmjfZ3xo6m9hdHhtYhg==} + '@fontsource-variable/source-serif-4@5.2.9': + resolution: {integrity: sha512-PPcxjLFk/fS0WHg79pDM2YNvz61kC+oYZ5cWZZyCS0DHpJncmuYOuiZAsvj4tDxlWPBEvxxcRLQQNmSaRbPkqw==} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -2172,9 +2172,9 @@ snapshots: '@exodus/bytes@1.6.0': {} - '@fontsource-variable/alegreya@5.2.8': {} + '@fontsource-variable/figtree@5.2.10': {} - '@fontsource-variable/lora@5.2.8': {} + '@fontsource-variable/source-serif-4@5.2.9': {} '@humanfs/core@0.19.1': {} diff --git a/web/src/components/ui/tests/EmptyState.test.tsx b/web/src/components/ui/tests/EmptyState.test.tsx new file mode 100644 index 0000000..7ef918c --- /dev/null +++ b/web/src/components/ui/tests/EmptyState.test.tsx @@ -0,0 +1,33 @@ +import { cleanup, render, screen } from "@solidjs/testing-library"; +import { afterEach, describe, expect, it } from "vitest"; +import { EmptyState } from "../EmptyState"; + +describe("EmptyState", () => { + afterEach(cleanup); + + it("renders title", () => { + render(() => ); + expect(screen.getByText("No items")).toBeInTheDocument(); + }); + + it("renders description when provided", () => { + render(() => ); + expect(screen.getByText("Create your first item")).toBeInTheDocument(); + }); + + it("renders custom icon when provided", () => { + render(() => 🎯} />); + expect(screen.getByTestId("custom-icon")).toBeInTheDocument(); + }); + + it("renders action when provided", () => { + render(() => Create} />); + expect(screen.getByRole("button", { name: "Create" })).toBeInTheDocument(); + }); + + it("renders default icon when no custom icon provided", () => { + render(() => ); + const svg = document.querySelector("svg"); + expect(svg).toBeInTheDocument(); + }); +}); diff --git a/web/src/fonts.d.ts b/web/src/fonts.d.ts index b9a63f0..5d89e8e 100644 --- a/web/src/fonts.d.ts +++ b/web/src/fonts.d.ts @@ -1,3 +1,3 @@ // CSS-only packages without TypeScript declarations -declare module "@fontsource-variable/alegreya"; -declare module "@fontsource-variable/lora"; +declare module "@fontsource-variable/figtree"; +declare module "@fontsource-variable/source-serif-4"; diff --git a/web/src/index.css b/web/src/index.css index 2378ab5..3ad16c2 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -2,8 +2,8 @@ @plugin "@egoist/tailwindcss-icons"; @theme { - --font-display: "Lora Variable", serif; - --font-body: "Alegreya Variable", serif; + --font-display: "Source Serif 4 Variable", serif; + --font-body: "Figtree Variable", serif; } * { diff --git a/web/src/index.tsx b/web/src/index.tsx index ba09891..9c7bd54 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,6 +1,6 @@ /* @refresh reload */ -import "@fontsource-variable/alegreya"; -import "@fontsource-variable/lora"; +import "@fontsource-variable/figtree"; +import "@fontsource-variable/source-serif-4"; import { render } from "solid-js/web"; import "./index.css"; import App from "./App.tsx"; diff --git a/web/src/pages/DeckNew.tsx b/web/src/pages/DeckNew.tsx index f3c91bb..59a5f32 100644 --- a/web/src/pages/DeckNew.tsx +++ b/web/src/pages/DeckNew.tsx @@ -4,6 +4,7 @@ import type { CreateDeckPayload } from "$lib/model"; import { toast } from "$lib/toast"; import { useNavigate } from "@solidjs/router"; import type { Component } from "solid-js"; +import { Motion } from "solid-motionone"; const DeckNew: Component = () => { const navigate = useNavigate(); @@ -26,13 +27,26 @@ const DeckNew: Component = () => { }; return ( -
-
-

Create New Deck

+ + +

Create New Deck

Start a new collection of flashcards.

-
- -
+ + + + + ); }; diff --git a/web/src/pages/DeckView.tsx b/web/src/pages/DeckView.tsx index a2d32e4..ffe00ef 100644 --- a/web/src/pages/DeckView.tsx +++ b/web/src/pages/DeckView.tsx @@ -1,13 +1,36 @@ import { CommentSection } from "$components/social/CommentSection"; import { FollowButton } from "$components/social/FollowButton"; import { Button } from "$components/ui/Button"; +import { Card } from "$components/ui/Card"; import { Dialog } from "$components/ui/Dialog"; +import { EmptyState } from "$components/ui/EmptyState"; +import { Skeleton } from "$components/ui/Skeleton"; +import { Tag } from "$components/ui/Tag"; import { api } from "$lib/api"; -import type { Card, Deck } from "$lib/model"; +import type { Card as CardType, Deck } from "$lib/model"; import { toast } from "$lib/toast"; import { A, useNavigate, useParams } from "@solidjs/router"; import type { Component } from "solid-js"; -import { createResource, createSignal, For, Show } from "solid-js"; +import { createResource, createSignal, For, Index, Show } from "solid-js"; +import { Motion } from "solid-motionone"; + +const CardSkeleton: Component = () => ( + +
+ +
+
+
+ + +
+
+ + +
+
+
+); const DeckView: Component = () => { const params = useParams(); @@ -15,11 +38,11 @@ const DeckView: Component = () => { const [showForkDialog, setShowForkDialog] = createSignal(false); const [deck] = createResource(() => params.id, async (id) => { const res = await api.getDeck(id); - return res.ok ? (await res.json() as Deck) : null; + return res.ok ? ((await res.json()) as Deck) : null; }); const [cards] = createResource(() => params.id, async (id) => { const res = await api.getDeckCards(id); - return res.ok ? (await res.json() as Card[]) : []; + return res.ok ? ((await res.json()) as CardType[]) : []; }); const handleFork = async () => { @@ -43,24 +66,48 @@ const DeckView: Component = () => { }; return ( -
- Loading deck...
}> + + + + + +
+ + +
+ + }> - Deck not found or you don't have access. - + } + action={ + + + + } /> }> - {deckValue => ( + {(deckValue) => ( <> -
+
-

{deckValue().title}

+

{deckValue().title}

- - {deckValue().visibility.type} - +
@@ -72,76 +119,78 @@ const DeckView: Component = () => {

{deckValue().description}

0}> -
- - {(tag) => ( - - #{tag} - - )} - +
+ {(tag) => }
- - + - - Back to Library + +
-
-
+ + +

- Cards {value => value().length} + Cards {(value) => ({value().length})}

- -
Loading cards...
+ {() => }}> +
+ } /> + }> + {(card, i) => ( + + +
+ CARD {i() + 1} +
+
+
+
Front
+
{card.front}
+
+
+
Back
+
+ {card.back || Empty} +
+
+
+
+
+ )} +
+
+
-
- - No cards in this deck. -
- }> - {(card, i) => ( -
-
- CARD {i() + 1} -
-
-
-
Front
-
{card.front}
-
-
-
Back
-
- {card.back || Empty} -
-
-
-
- )} - -
-
-
+ -
+
)} @@ -157,12 +206,12 @@ const DeckView: Component = () => { }> -

Are you sure you want to fork "{deck()?.title}"?

-

+

Are you sure you want to fork "{deck()?.title}"?

+

This will create a copy of this deck in your library that you can study and edit.

- + ); }; diff --git a/web/src/pages/Discovery.tsx b/web/src/pages/Discovery.tsx index 9ce656d..8e2b3e2 100644 --- a/web/src/pages/Discovery.tsx +++ b/web/src/pages/Discovery.tsx @@ -1,64 +1,77 @@ import { SearchInput } from "$components/SearchInput"; +import { Skeleton } from "$components/ui/Skeleton"; +import { Tag } from "$components/ui/Tag"; import { api } from "$lib/api"; import { A } from "@solidjs/router"; import type { Component } from "solid-js"; -import { createResource, For, Show } from "solid-js"; +import { createResource, For, Index, Show } from "solid-js"; +import { Motion } from "solid-motionone"; -// TODO: type discovery response const Discovery: Component = () => { const [data] = createResource(async () => { const res = await api.getDiscovery(); - if (res.ok) return await res.json(); + if (res.ok) return (await res.json()) as { top_tags: [string, number][] }; return { top_tags: [] }; }); return ( -
-
-

- Discover Malfestio -

-

Explore community decks and popular topics

-
+ + +

Discover Malfestio

+

Explore community decks and popular topics

+
-
+ -
-

-
+ +

+ Top Tags

-
+
+ {() => }
}>
- {(tag: [string, number]) => ( - - - #{tag[0]} - - - {tag[1]} - - + {(tag, i) => ( + + + + {tag[1]} + + )} -

No tags found yet. Create some decks!

+

No tags found yet. Create some decks!

-
-
+ + ); }; diff --git a/web/src/pages/Feed.tsx b/web/src/pages/Feed.tsx index c22a5c1..8a82f60 100644 --- a/web/src/pages/Feed.tsx +++ b/web/src/pages/Feed.tsx @@ -2,12 +2,16 @@ import { FollowButton } from "$components/social/FollowButton"; import { Button } from "$components/ui/Button"; import { Card } from "$components/ui/Card"; import { Dialog } from "$components/ui/Dialog"; +import { EmptyState } from "$components/ui/EmptyState"; +import { Skeleton } from "$components/ui/Skeleton"; import { Tabs } from "$components/ui/Tabs"; +import { Tag } from "$components/ui/Tag"; import { api } from "$lib/api"; import type { Deck } from "$lib/model"; import { toast } from "$lib/toast"; import { A, useNavigate } from "@solidjs/router"; -import { createResource, createSignal, For, Match, Show, Switch } from "solid-js"; +import { createResource, createSignal, For, Index, Match, Show, Switch } from "solid-js"; +import { Motion } from "solid-motionone"; export default function Feed() { const navigate = useNavigate(); @@ -15,12 +19,12 @@ export default function Feed() { const [followsFeed] = createResource(async () => { const res = await api.getFeedFollows(); - return res.ok ? (await res.json() as Deck[]) : []; + return res.ok ? ((await res.json()) as Deck[]) : []; }); const [valuableFeed] = createResource(async () => { const res = await api.getFeedTrending(); - return res.ok ? (await res.json() as Deck[]) : []; + return res.ok ? ((await res.json()) as Deck[]) : []; }); const handleFork = async () => { @@ -43,64 +47,104 @@ export default function Feed() { } }; - const DeckItem = (props: { deck: Deck }) => ( + const DeckItem = (props: { deck: Deck; index: number }) => ( + + +
+
+

{props.deck.title}

+

+ By {props.deck.owner_did} •{" "} + + {(published_at) => new Date(published_at()).toLocaleDateString()} + +

+

{props.deck.description}

+
+ {(tag) => } +
+
+
+ +
+
+
+ + + + +
+
+
+ ); + + const DeckSkeleton = () => (
-
-

{props.deck.title}

-

- By {props.deck.owner_did} •{" "} - - {published_at => new Date(published_at()).toLocaleDateString()} - -

-

{props.deck.description}

-
- - {(tag) => {tag}} - +
+ + + +
+ +
-
- -
-
-
- - - - +
); return ( -
-

Discovery

+ + +

Discovery

+

Explore content from people you follow and trending decks.

+
+ {(activeTab) => ( -
- - {feed => ( - No updates from followed users.
}> - {(deck) => } - - )} +
+ {() => }}> + } /> + }> + {(deck, i) => } +
-
- - {feed => ( - No trending decks.
}> - {(deck) => } - - )} +
+ {() => }}> + } /> + }> + {(deck, i) => } +
@@ -118,9 +162,9 @@ export default function Feed() { }> -

Are you sure you want to fork "{forkDialogDeck()?.title}"?

-

This will create a copy of this deck in your library.

+

Are you sure you want to fork "{forkDialogDeck()?.title}"?

+

This will create a copy of this deck in your library.

-
+ ); } diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index f38f161..e3074a3 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,82 +1,115 @@ +import { Card } from "$components/ui/Card"; +import { EmptyState } from "$components/ui/EmptyState"; +import { Skeleton } from "$components/ui/Skeleton"; +import { Tag } from "$components/ui/Tag"; import { api } from "$lib/api"; import type { Deck } from "$lib/model"; +import { Button } from "$ui/Button"; import { A } from "@solidjs/router"; import type { Component } from "solid-js"; -import { createResource, For, Show } from "solid-js"; +import { createResource, For, Index, Show } from "solid-js"; +import { Motion } from "solid-motionone"; -const DeckCard: Component<{ deck: Deck }> = (props) => ( -
+const DeckCard: Component<{ deck: Deck; index: number }> = (props) => ( + + +
+

+ {props.deck.title} +

+ + + +
+

{props.deck.description}

+ +
+ {(tag) => } +
+ + +
+
+); + +const DeckCardSkeleton: Component = () => ( +
-

- {props.deck.title} -

- - - {props.deck.visibility.type} - - +
-

{props.deck.description}

- -
- - {(tag) => #{tag}} - +
+ +
- - +
+ +
+ ); const Home: Component = () => { const [decks] = createResource(async () => { const res = await api.getDecks(); - return res.ok ? (await res.json() as Deck[]) : []; + return res.ok ? ((await res.json()) as Deck[]) : []; }); return ( -
-
+ +
-

Library

+

Library

Manage your study decks and discover new content.

- - + Create Deck + + -
+
- - Loading library... -
- }> + {() => }}> -

No decks found

-

- Create your first deck to get started with spaced repetition learning. -

+
+ } + action={ + + + + } />
}> - {(deck) => } + {(deck, i) => }
-
+ ); }; diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx index a3e6951..1329b8a 100644 --- a/web/src/pages/Landing.tsx +++ b/web/src/pages/Landing.tsx @@ -44,7 +44,7 @@ const steps = [{ title: "Import", desc: "Create cards from articles, lectures, o const Feature: Component<{ title: string; desc: string; icon: JSX.Element }> = (props) => (
{props.icon}
-

{props.title}

+

{props.title}

{props.desc}

); @@ -134,9 +134,9 @@ const Landing: Component = () => ( initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }} - class="text-5xl md:text-7xl font-light tracking-tight mb-8 leading-[1.1]"> + class="text-7xl md:text-8xl font-medium tracking-tight mb-8 leading-[1.1]"> Learning on
- the AT Protocol. +

the AT Protocol.