From cdb9c59d37c39ec3f25d4094e21bdac011a30faa Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 8 Apr 2026 02:30:57 +0200 Subject: [PATCH] feat(app): add AddSongSheet component --- app/app/components/add-song-sheet.tsx | 155 ++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 app/app/components/add-song-sheet.tsx diff --git a/app/app/components/add-song-sheet.tsx b/app/app/components/add-song-sheet.tsx new file mode 100644 index 0000000..25d8f9c --- /dev/null +++ b/app/app/components/add-song-sheet.tsx @@ -0,0 +1,155 @@ +import { useRef, useState } from "react"; +import { useNavigate } from "react-router"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "~/components/ui/sheet"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import type { Song, SongSummary } from "~/lib/types"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + onSongAdded: (summary: SongSummary) => void; +} + +function previewChords(song: Song): string[] { + const seen = new Set(); + const result: string[] = []; + for (const section of song.sections) { + for (const line of section.lines) { + for (const cp of line.chords) { + if (!seen.has(cp.chord)) { + seen.add(cp.chord); + result.push(cp.chord); + } + } + } + if (result.length >= 5) break; + } + return result.slice(0, 5); +} + +export function AddSongSheet({ open, onOpenChange, onSongAdded }: Props) { + const navigate = useNavigate(); + const [url, setUrl] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const fileRef = useRef(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + const file = fileRef.current?.files?.[0]; + if (!url.trim() && !file) { + setError("Provide a URL or pick a file."); + return; + } + + setLoading(true); + try { + if (file) { + // File paths can't be sent from the browser — the API expects a file:// URI + // which only works when running locally. Show a helpful error. + throw new Error("File upload requires running the app locally with direct file paths. Use a URL instead."); + } + + const resp = await fetch("http://localhost:8000/tabs/parse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ source: url.trim() }), + }); + + 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); + onOpenChange(false); + setUrl(""); + navigate(`/songs/${id}`); + } catch (err) { + setError(err instanceof Error ? err.message : "Something went wrong."); + } finally { + setLoading(false); + } + } + + return ( + + + + Add Song + +
+
+ + setUrl(e.target.value)} + disabled={loading} + /> +
+ +
+
+ or +
+
+ +
+ + + +
+ + {error && ( +

{error}

+ )} + +
+ + +
+ + + + ); +}