learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs

refactor: centralize API calls

Changed files
+102 -126
web
+39 -68
web/src/components/ReviewStats.tsx
··· 1 1 import { fadeIn } from "$lib/animations"; 2 - import { api } from "$lib/api"; 3 - import type { ReviewCard, StudyStats } from "$lib/model"; 2 + import type { StudyStats } from "$lib/model"; 4 3 import { Skeleton } from "$ui/Skeleton"; 5 4 import { type Component, Show } from "solid-js"; 6 5 import { Motion } from "solid-motionone"; 7 6 8 7 type ReviewStatsProps = { stats: StudyStats | null; loading?: boolean }; 9 8 10 - export const ReviewStats: Component<ReviewStatsProps> = (props) => { 11 - return ( 12 - <Motion.div {...fadeIn} class="bg-gray-900 rounded-xl p-6 border border-gray-800"> 13 - <Show 14 - when={!props.loading} 15 - fallback={ 9 + export const ReviewStats: Component<ReviewStatsProps> = (props) => ( 10 + <Motion.div {...fadeIn} class="bg-gray-900 rounded-xl p-6 border border-gray-800"> 11 + <Show 12 + when={!props.loading} 13 + fallback={ 14 + <div class="space-y-4"> 15 + <Skeleton class="h-6 w-32" /> 16 + <Skeleton class="h-4 w-48" /> 17 + <Skeleton class="h-4 w-40" /> 18 + </div> 19 + }> 20 + <Show when={props.stats} fallback={<p class="text-gray-400">No stats available</p>}> 21 + {stats => ( 16 22 <div class="space-y-4"> 17 - <Skeleton class="h-6 w-32" /> 18 - <Skeleton class="h-4 w-48" /> 19 - <Skeleton class="h-4 w-40" /> 20 - </div> 21 - }> 22 - <Show when={props.stats} fallback={<p class="text-gray-400">No stats available</p>}> 23 - {stats => ( 24 - <div class="space-y-4"> 25 - <div class="flex items-center justify-between"> 26 - <h3 class="text-lg font-semibold text-white">Study Progress</h3> 27 - {/* TODO: fire icon */} 28 - <span class="text-2xl">🔥 {stats().current_streak} day streak</span> 23 + <div class="flex items-center justify-between"> 24 + <h3 class="text-lg font-semibold text-white">Study Progress</h3> 25 + {/* TODO: fire icon */} 26 + <span class="text-2xl">🔥 {stats().current_streak} day streak</span> 27 + </div> 28 + 29 + <div class="grid grid-cols-3 gap-4 text-center"> 30 + <div class="bg-gray-800 rounded-lg p-4"> 31 + <p class="text-3xl font-bold text-blue-400">{stats().due_count}</p> 32 + <p class="text-sm text-gray-400">Due Today</p> 33 + </div> 34 + <div class="bg-gray-800 rounded-lg p-4"> 35 + <p class="text-3xl font-bold text-green-400">{stats().reviewed_today}</p> 36 + <p class="text-sm text-gray-400">Reviewed</p> 29 37 </div> 30 - 31 - <div class="grid grid-cols-3 gap-4 text-center"> 32 - <div class="bg-gray-800 rounded-lg p-4"> 33 - <p class="text-3xl font-bold text-blue-400">{stats().due_count}</p> 34 - <p class="text-sm text-gray-400">Due Today</p> 35 - </div> 36 - <div class="bg-gray-800 rounded-lg p-4"> 37 - <p class="text-3xl font-bold text-green-400">{stats().reviewed_today}</p> 38 - <p class="text-sm text-gray-400">Reviewed</p> 39 - </div> 40 - <div class="bg-gray-800 rounded-lg p-4"> 41 - <p class="text-3xl font-bold text-purple-400">{stats().total_reviews}</p> 42 - <p class="text-sm text-gray-400">Total</p> 43 - </div> 38 + <div class="bg-gray-800 rounded-lg p-4"> 39 + <p class="text-3xl font-bold text-purple-400">{stats().total_reviews}</p> 40 + <p class="text-sm text-gray-400">Total</p> 44 41 </div> 45 - <Show when={stats().longest_streak > 0}> 46 - <p class="text-sm text-gray-500 text-center">Longest streak: {stats().longest_streak} days</p> 47 - </Show> 48 42 </div> 49 - )} 50 - </Show> 43 + <Show when={stats().longest_streak > 0}> 44 + <p class="text-sm text-gray-500 text-center">Longest streak: {stats().longest_streak} days</p> 45 + </Show> 46 + </div> 47 + )} 51 48 </Show> 52 - </Motion.div> 53 - ); 54 - }; 55 - 56 - // TODO: move this to api.ts 57 - export async function fetchStudyStats(): Promise<StudyStats | null> { 58 - try { 59 - const response = await api.getStats(); 60 - if (response.ok) { 61 - return response.json(); 62 - } 63 - } catch (err) { 64 - console.error("Failed to fetch stats:", err); 65 - } 66 - return null; 67 - } 68 - 69 - // TODO: move this to api.ts 70 - export async function fetchDueCards(deckId?: string): Promise<ReviewCard[]> { 71 - try { 72 - const response = await api.getDueCards(deckId); 73 - if (response.ok) { 74 - return response.json(); 75 - } 76 - } catch (err) { 77 - console.error("Failed to fetch due cards:", err); 78 - } 79 - return []; 80 - } 49 + </Show> 50 + </Motion.div> 51 + );
+1 -10
web/src/components/social/CommentSection.tsx
··· 1 1 import { api } from "$lib/api"; 2 + import type { Comment } from "$lib/model"; 2 3 import { authStore } from "$lib/store"; 3 4 import { Button } from "$ui/Button"; 4 5 import { createResource, createSignal, For, Show } from "solid-js"; 5 - 6 - // TODO: move to model.ts 7 - type Comment = { 8 - id: string; 9 - deck_id: string; 10 - author_did: string; 11 - content: string; 12 - parent_id: string | null; 13 - created_at: string; 14 - }; 15 6 16 7 type CommentNode = { comment: Comment; children: CommentNode[] }; 17 8
+30 -4
web/src/lib/api.ts
··· 1 + import type { CreateDeckPayload } from "./model"; 1 2 import { authStore } from "./store"; 2 3 3 4 const API_BASE = "/api"; ··· 32 33 if (deckId) params.set("deck_id", deckId); 33 34 return apiFetch(`/review/due?${params}`, { method: "GET" }); 34 35 }, 35 - submitReview: (cardId: string, grade: number) => 36 - apiFetch("/review/submit", { method: "POST", body: JSON.stringify({ card_id: cardId, grade }) }), 36 + submitReview: (cardId: string, grade: number) => { 37 + return apiFetch("/review/submit", { method: "POST", body: JSON.stringify({ card_id: cardId, grade }) }); 38 + }, 37 39 getStats: () => apiFetch("/review/stats", { method: "GET" }), 38 40 follow: (did: string) => apiFetch(`/social/follow/${did}`, { method: "POST" }), 39 41 unfollow: (did: string) => apiFetch(`/social/unfollow/${did}`, { method: "POST" }), 40 42 getFollowers: (did: string) => apiFetch(`/social/followers/${did}`, { method: "GET" }), 41 43 getFollowing: (did: string) => apiFetch(`/social/following/${did}`, { method: "GET" }), 42 - addComment: (deckId: string, content: string, parentId?: string) => 43 - apiFetch(`/decks/${deckId}/comments`, { method: "POST", body: JSON.stringify({ content, parent_id: parentId }) }), 44 + addComment: (deckId: string, content: string, parentId?: string) => { 45 + return apiFetch(`/decks/${deckId}/comments`, { 46 + method: "POST", 47 + body: JSON.stringify({ content, parent_id: parentId }), 48 + }); 49 + }, 44 50 getComments: (deckId: string) => apiFetch(`/decks/${deckId}/comments`, { method: "GET" }), 45 51 getFeedFollows: () => apiFetch("/feeds/follows", { method: "GET" }), 46 52 getFeedTrending: () => apiFetch("/feeds/trending", { method: "GET" }), 47 53 forkDeck: (deckId: string) => apiFetch(`/decks/${deckId}/fork`, { method: "POST" }), 54 + getDecks: () => apiFetch("/decks", { method: "GET" }), 55 + getDeck: (id: string) => apiFetch(`/decks/${id}`, { method: "GET" }), 56 + getDeckCards: (id: string) => apiFetch(`/decks/${id}/cards`, { method: "GET" }), 57 + createDeck: async (payload: CreateDeckPayload) => { 58 + const { cards, ...deckPayload } = payload; 59 + const res = await apiFetch("/decks", { method: "POST", body: JSON.stringify(deckPayload) }); 60 + if (!res.ok) return res; 61 + 62 + const deck = await res.json(); 63 + if (cards && cards.length > 0) { 64 + await Promise.all(cards.map((c) => 65 + apiFetch("/cards", { 66 + method: "POST", 67 + body: JSON.stringify({ deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl }), 68 + }) 69 + )); 70 + } 71 + 72 + return { ok: true, json: async () => deck }; 73 + }, 48 74 };
+11
web/src/lib/model.ts
··· 58 58 }; 59 59 60 60 export type ReviewResponse = { ease_factor: number; interval_days: number; repetitions: number; due_at: string }; 61 + 62 + export type Comment = { 63 + id: string; 64 + deck_id: string; 65 + author_did: string; 66 + content: string; 67 + parent_id: string | null; 68 + created_at: string; 69 + }; 70 + 71 + export type CommentNode = { comment: Comment; children: CommentNode[] };
+2 -14
web/src/pages/DeckNew.tsx
··· 1 1 import { DeckEditor } from "$components/DeckEditor"; 2 2 import { api } from "$lib/api"; 3 - import type { Card, CreateDeckPayload } from "$lib/model"; 3 + import type { CreateDeckPayload } from "$lib/model"; 4 4 import { toast } from "$lib/toast"; 5 5 import { useNavigate } from "@solidjs/router"; 6 6 import type { Component } from "solid-js"; ··· 9 9 const navigate = useNavigate(); 10 10 11 11 const handleSave = async (data: CreateDeckPayload) => { 12 - // TODO: some of this can be in api.ts 13 12 try { 14 - const { cards, ...deckPayload } = data; 15 - const res = await api.post("/decks", deckPayload); 16 - 13 + const res = await api.createDeck(data); 17 14 if (res.ok) { 18 15 const deck = await res.json(); 19 - 20 - if (cards && cards.length > 0) { 21 - await Promise.all( 22 - cards.map((c: Card) => 23 - api.post("/cards", { deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl }) 24 - ), 25 - ); 26 - } 27 - 28 16 toast.success("Deck created successfully"); 29 17 navigate(`/decks/${deck.id}`); 30 18 } else {
+8 -16
web/src/pages/DeckView.tsx
··· 7 7 import type { Component } from "solid-js"; 8 8 import { createResource, For, Show } from "solid-js"; 9 9 10 - // TODO: use api.ts 11 - const fetchDeck = async (id: string): Promise<Deck | null> => { 12 - const res = await api.get(`/decks/${id}`); 13 - if (!res.ok) return null; 14 - return res.json(); 15 - }; 16 - 17 - // TODO: use api.ts 18 - const fetchCards = async (id: string): Promise<Card[]> => { 19 - const res = await api.get(`/decks/${id}/cards`); 20 - if (!res.ok) return []; 21 - return res.json(); 22 - }; 23 - 24 10 const DeckView: Component = () => { 25 11 const params = useParams(); 26 - const [deck] = createResource(() => params.id, fetchDeck); 27 - const [cards] = createResource(() => params.id, fetchCards); 12 + const [deck] = createResource(() => params.id, async (id) => { 13 + const res = await api.getDeck(id); 14 + return res.ok ? (await res.json() as Deck) : null; 15 + }); 16 + const [cards] = createResource(() => params.id, async (id) => { 17 + const res = await api.getDeckCards(id); 18 + return res.ok ? (await res.json() as Card[]) : []; 19 + }); 28 20 29 21 const handleFork = async () => { 30 22 if (!deck()) return;
+4 -8
web/src/pages/Home.tsx
··· 4 4 import type { Component } from "solid-js"; 5 5 import { createResource, For, Show } from "solid-js"; 6 6 7 - // TODO: use api.ts 8 - const fetchDecks = async (): Promise<Deck[]> => { 9 - const res = await api.get("/decks"); 10 - if (!res.ok) return []; 11 - return res.json(); 12 - }; 13 - 14 7 const DeckCard: Component<{ deck: Deck }> = (props) => ( 15 8 <div class="bg-[#262626] border border-[#393939] p-4 hover:border-[#0F62FE] transition-colors group relative h-full flex flex-col"> 16 9 <div class="flex justify-between items-start mb-2"> ··· 42 35 ); 43 36 44 37 const Home: Component = () => { 45 - const [decks] = createResource(fetchDecks); 38 + const [decks] = createResource(async () => { 39 + const res = await api.getDecks(); 40 + return res.ok ? (await res.json() as Deck[]) : []; 41 + }); 46 42 47 43 return ( 48 44 <div class="max-w-7xl mx-auto px-0 py-8">
+7 -6
web/src/pages/Review.tsx
··· 1 - import { fetchDueCards, fetchStudyStats, ReviewStats } from "$components/ReviewStats"; 1 + import { ReviewStats } from "$components/ReviewStats"; 2 2 import { StudySession } from "$components/StudySession"; 3 3 import { fadeIn } from "$lib/animations"; 4 + import { api } from "$lib/api"; 4 5 import type { ReviewCard, StudyStats as StudyStatsType } from "$lib/model"; 5 6 import { Button } from "$ui/Button"; 6 7 import { Skeleton } from "$ui/Skeleton"; ··· 19 20 const [sessionComplete, setSessionComplete] = createSignal(false); 20 21 21 22 onMount(async () => { 22 - const [statsData, cardsData] = await Promise.all([fetchStudyStats(), fetchDueCards(params.deckId)]); 23 - setStats(statsData); 24 - setCards(cardsData); 23 + const [statsRes, cardsRes] = await Promise.all([api.getStats(), api.getDueCards(params.deckId)]); 24 + if (statsRes.ok) setStats(await statsRes.json()); 25 + if (cardsRes.ok) setCards(await cardsRes.json()); 25 26 setLoading(false); 26 27 }); 27 28 ··· 35 36 const handleComplete = async () => { 36 37 setSessionActive(false); 37 38 setSessionComplete(true); 38 - const newStats = await fetchStudyStats(); 39 - setStats(newStats); 39 + const res = await api.getStats(); 40 + if (res.ok) setStats(await res.json()); 40 41 }; 41 42 42 43 const handleExit = () => {