diff --git a/app/app/components/add-song-sheet.tsx b/app/app/components/add-song-sheet.tsx index 3e97be8..bee36d1 100644 --- a/app/app/components/add-song-sheet.tsx +++ b/app/app/components/add-song-sheet.tsx @@ -8,7 +8,8 @@ import { } from "~/components/ui/sheet"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; -import type { Song, SongSummary } from "~/lib/types"; +import type { SongSummary } from "~/lib/types"; +import { createSong } from "~/lib/api"; import { previewChords } from "~/lib/mock"; interface Props { @@ -58,35 +59,13 @@ export function AddSongSheet({ open, onOpenChange, onSongAdded }: Props) { setLoading(true); try { - const apiBase = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; - const body = fileHtml - ? { html: fileHtml } - : { source: url.trim() }; - - const resp = await fetch(`${apiBase}/tabs/parse`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!resp.ok) { - const data = await resp.json().catch(() => ({})); - throw new Error((data as { error?: string }).error ?? `HTTP ${resp.status}`); - } - - const song: Song = await resp.json(); - const id = `new-${Date.now()}`; - sessionStorage.setItem(id, JSON.stringify(song)); - - const summary: SongSummary = { - id, - meta: song.meta, - preview_chords: previewChords(song), - }; - onSongAdded(summary); + const stored = await createSong( + fileHtml ? { html: fileHtml } : { source: url.trim() } + ); + onSongAdded({ id: stored.id, meta: stored.song.meta, preview_chords: previewChords(stored.song) }); onOpenChange(false); reset(); - navigate(`/songs/${id}`); + navigate(`/songs/${stored.id}`); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong."); } finally { diff --git a/app/app/lib/api.ts b/app/app/lib/api.ts new file mode 100644 index 0000000..c30dc07 --- /dev/null +++ b/app/app/lib/api.ts @@ -0,0 +1,45 @@ +import type { Song, SongSummary, StoredSong } from "./types"; + +function getApiBase(): string { + // Works in both SSR (Node/process.env) and client (import.meta.env) + if (typeof process !== "undefined" && process.env?.API_URL) { + return process.env.API_URL; + } + if (typeof import.meta !== "undefined" && (import.meta as any).env?.VITE_API_URL) { + return (import.meta as any).env.VITE_API_URL; + } + return "http://localhost:8000"; +} + +export async function listSongs(): Promise { + const res = await fetch(`${getApiBase()}/songs`); + if (!res.ok) throw new Error(`Failed to load songs: ${res.status}`); + return res.json(); +} + +export async function getSong(id: string): Promise { + const res = await fetch(`${getApiBase()}/songs/${id}`); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`Failed to load song: ${res.status}`); + return res.json(); +} + +export async function createSong(body: { + source?: string; + html?: string; +}): Promise { + const res = await fetch(`${getApiBase()}/songs`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error((data as { error?: string }).error ?? `HTTP ${res.status}`); + } + return res.json(); +} + +export async function deleteSong(id: string): Promise { + await fetch(`${getApiBase()}/songs/${id}`, { method: "DELETE" }); +} diff --git a/app/app/lib/types.ts b/app/app/lib/types.ts index eb79291..3498231 100644 --- a/app/app/lib/types.ts +++ b/app/app/lib/types.ts @@ -35,3 +35,8 @@ export interface SongSummary { // First 5 unique chord names from the song, in order of appearance preview_chords: string[]; } + +export interface StoredSong { + id: string; + song: Song; +} diff --git a/app/app/routes/home.tsx b/app/app/routes/home.tsx index 0fbb43e..8261a76 100644 --- a/app/app/routes/home.tsx +++ b/app/app/routes/home.tsx @@ -6,7 +6,7 @@ import { Card, CardContent } from "~/components/ui/card"; import { Plus } from "lucide-react"; import { SongCard } from "~/components/song-card"; import { AddSongSheet } from "~/components/add-song-sheet"; -import { MOCK_SONGS } from "~/lib/mock"; +import { listSongs } from "~/lib/api"; import type { SongSummary } from "~/lib/types"; export function meta({}: Route.MetaArgs) { @@ -16,8 +16,13 @@ export function meta({}: Route.MetaArgs) { ]; } -export function loader() { - return { songs: MOCK_SONGS }; +export async function loader() { + try { + const songs = await listSongs(); + return { songs }; + } catch { + return { songs: [] }; + } } export default function Home({ loaderData }: Route.ComponentProps) { diff --git a/app/app/routes/songs.$id.tsx b/app/app/routes/songs.$id.tsx index 1de38b2..5745799 100644 --- a/app/app/routes/songs.$id.tsx +++ b/app/app/routes/songs.$id.tsx @@ -4,8 +4,7 @@ import type { Route } from "./+types/songs.$id"; import { TransposeBar } from "~/components/transpose-bar"; import { ChordChart } from "~/components/chord-chart"; import { transposeSong } from "~/lib/transpose"; -import { getMockSong } from "~/lib/mock"; -import type { Song } from "~/lib/types"; +import { getSong } from "~/lib/api"; export function meta({ data }: Route.MetaArgs) { if (!data?.song) return [{ title: "PocketChords" }]; @@ -15,53 +14,21 @@ export function meta({ data }: Route.MetaArgs) { ]; } -export function loader({ params }: Route.LoaderArgs) { +export async function loader({ params }: Route.LoaderArgs) { const id = params.id ?? ""; - - // Temporary songs are stored in sessionStorage on the client. - // During SSR, we can't access sessionStorage — return a placeholder - // that the client will hydrate from sessionStorage. - if (id.startsWith("new-")) { - return { song: null as unknown as Song, tempId: id }; - } - - const song = getMockSong(id); + const song = await getSong(id); if (!song) throw data("Song not found", { status: 404 }); - return { song, tempId: null }; + return { song }; } export default function SongDetail({ loaderData }: Route.ComponentProps) { - const { tempId } = loaderData; - - // Resolve song — either from loader or from sessionStorage (temp songs) - const [song] = useState(() => { - if (loaderData.song) return loaderData.song; - if (tempId && typeof window !== "undefined") { - const stored = sessionStorage.getItem(tempId); - if (stored) return JSON.parse(stored) as Song; - } - return loaderData.song; - }); - + const { song } = loaderData; const [offset, setOffset] = useState(0); - - if (!song) { - return ( -
-

Song not found.

-
- ); - } - const displayed = transposeSong(song, offset); return (
- +