learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 130 lines 5.9 kB view raw
1import type { CreateDeckPayload, CreateNotePayload } from "./model"; 2import { authStore } from "./store"; 3 4const API_BASE = "/api"; 5 6export async function apiFetch(path: string, options: RequestInit = {}) { 7 const token = authStore.accessJwt(); 8 9 const headers = new Headers(options.headers); 10 if (token) { 11 headers.set("Authorization", `Bearer ${token}`); 12 } 13 14 if (options.body && typeof options.body === "string" && !headers.has("Content-Type")) { 15 headers.set("Content-Type", "application/json"); 16 } 17 18 const response = await fetch(`${API_BASE}${path}`, { ...options, headers }); 19 20 if (response.status === 401) { 21 authStore.logout(); 22 window.location.href = "/login"; 23 } 24 25 return response; 26} 27 28const syncMethods = { 29 pushDeck: (id: string) => apiFetch(`/sync/push/deck/${id}`, { method: "POST" }), 30 pushNote: (id: string) => apiFetch(`/sync/push/note/${id}`, { method: "POST" }), 31 getSyncStatus: () => apiFetch("/sync/status", { method: "GET" }), 32 resolveConflict: (entityType: string, id: string, strategy: "last_write_wins" | "keep_local" | "keep_remote") => { 33 return apiFetch(`/sync/resolve/${entityType}/${id}`, { method: "POST", body: JSON.stringify({ strategy }) }); 34 }, 35}; 36 37export const api = { 38 get: (path: string) => apiFetch(path, { method: "GET" }), 39 post: (path: string, body: unknown) => apiFetch(path, { method: "POST", body: JSON.stringify(body) }), 40 getStats: () => apiFetch("/review/stats", { method: "GET" }), 41 follow: (did: string) => apiFetch(`/social/follow/${did}`, { method: "POST" }), 42 unfollow: (did: string) => apiFetch(`/social/unfollow/${did}`, { method: "POST" }), 43 getFollowers: (did: string) => apiFetch(`/social/followers/${did}`, { method: "GET" }), 44 getFollowing: (did: string) => apiFetch(`/social/following/${did}`, { method: "GET" }), 45 getComments: (deckId: string) => apiFetch(`/decks/${deckId}/comments`, { method: "GET" }), 46 getFeedFollows: () => apiFetch("/feeds/follows", { method: "GET" }), 47 getFeedTrending: () => apiFetch("/feeds/trending", { method: "GET" }), 48 forkDeck: (deckId: string) => apiFetch(`/decks/${deckId}/fork`, { method: "POST" }), 49 getDecks: () => apiFetch("/decks", { method: "GET" }), 50 getDeck: (id: string) => apiFetch(`/decks/${id}`, { method: "GET" }), 51 getDeckCards: (id: string) => apiFetch(`/decks/${id}/cards`, { method: "GET" }), 52 getPreferences: () => apiFetch("/preferences", { method: "GET" }), 53 getDiscovery: () => apiFetch("/discovery", { method: "GET" }), 54 getUserProfile: (did: string) => apiFetch(`/users/${did}/profile`, { method: "GET" }), 55 getRemoteDeck: (uri: string) => apiFetch(`/remote/deck?uri=${encodeURIComponent(uri)}`, { method: "GET" }), 56 exportData: (collection: "decks" | "notes") => apiFetch(`/export/${collection}`, { method: "GET" }), 57 getNotes: () => apiFetch("/notes", { method: "GET" }), 58 getNote: (id: string) => apiFetch(`/notes/${id}`, { method: "GET" }), 59 createNote: (payload: CreateNotePayload) => apiFetch("/notes", { method: "POST", body: JSON.stringify(payload) }), 60 deleteNote: (id: string) => apiFetch(`/notes/${id}`, { method: "DELETE" }), 61 updateNote: (id: string, payload: object) => { 62 return apiFetch(`/notes/${id}`, { method: "PUT", body: JSON.stringify(payload) }); 63 }, 64 createDeck: async (payload: CreateDeckPayload) => { 65 const { cards, ...deckPayload } = payload; 66 const res = await apiFetch("/decks", { method: "POST", body: JSON.stringify(deckPayload) }); 67 if (!res.ok) return res; 68 69 const deck = await res.json(); 70 if (cards && cards.length > 0) { 71 await Promise.all( 72 cards.map((c) => 73 apiFetch("/cards", { 74 method: "POST", 75 body: JSON.stringify({ deck_id: deck.id, front: c.front, back: c.back, media_url: c.mediaUrl }), 76 }) 77 ), 78 ); 79 } 80 81 return { ok: true, json: async () => deck }; 82 }, 83 addComment: (deckId: string, content: string, parentId?: string) => { 84 return apiFetch(`/decks/${deckId}/comments`, { 85 method: "POST", 86 body: JSON.stringify({ content, parent_id: parentId }), 87 }); 88 }, 89 search: (query: string, limit = 20, offset = 0, source?: "local" | "remote") => { 90 const params = new URLSearchParams({ q: query, limit: String(limit), offset: String(offset) }); 91 if (source) params.set("source", source); 92 return apiFetch(`/search?${params}`, { method: "GET" }); 93 }, 94 getDueCards: (deckId?: string, limit = 20) => { 95 const params = new URLSearchParams({ limit: String(limit) }); 96 if (deckId) params.set("deck_id", deckId); 97 return apiFetch(`/review/due?${params}`, { method: "GET" }); 98 }, 99 submitReview: (cardId: string, grade: number) => { 100 return apiFetch("/review/submit", { method: "POST", body: JSON.stringify({ card_id: cardId, grade }) }); 101 }, 102 updatePreferences: (updates: import("./model").UpdatePreferencesPayload) => { 103 return apiFetch("/preferences", { method: "PUT", body: JSON.stringify(updates) }); 104 }, 105 startOAuth: async (handle: string) => { 106 const res = await apiFetch("/oauth/authorize", { method: "POST", body: JSON.stringify({ handle }) }); 107 if (res.ok) { 108 const data = await res.json(); 109 window.location.href = data.authorization_url; 110 return { ok: true }; 111 } 112 return res; 113 }, 114 // TODO: type check visibility 115 saveImportedArticle: (payload: { url: string; tags?: string[]; visibility?: unknown }) => { 116 return apiFetch("/import/article/save", { method: "POST", body: JSON.stringify(payload) }); 117 }, 118 downloadNoteAsMarkdown: (note: { title: string; body: string }) => { 119 const blob = new Blob([note.body], { type: "text/markdown" }); 120 const url = URL.createObjectURL(blob); 121 const a = document.createElement("a"); 122 a.href = url; 123 a.download = `${note.title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md`; 124 document.body.appendChild(a); 125 a.click(); 126 document.body.removeChild(a); 127 URL.revokeObjectURL(url); 128 }, 129 ...syncMethods, 130};