diff --git a/app/app/lib/api.ts b/app/app/lib/api.ts index c30dc07..0a5230d 100644 --- a/app/app/lib/api.ts +++ b/app/app/lib/api.ts @@ -1,4 +1,4 @@ -import type { Song, SongSummary, StoredSong } from "./types"; +import type { Song, SongSummary, StoredSong, UpdateSongRequest } from "./types"; function getApiBase(): string { // Works in both SSR (Node/process.env) and client (import.meta.env) @@ -11,8 +11,11 @@ function getApiBase(): string { return "http://localhost:8000"; } -export async function listSongs(): Promise { - const res = await fetch(`${getApiBase()}/songs`); +export async function listSongs(q = ""): Promise { + const url = q.trim() + ? `${getApiBase()}/songs?q=${encodeURIComponent(q.trim())}` + : `${getApiBase()}/songs`; + const res = await fetch(url); if (!res.ok) throw new Error(`Failed to load songs: ${res.status}`); return res.json(); } @@ -43,3 +46,16 @@ export async function createSong(body: { export async function deleteSong(id: string): Promise { await fetch(`${getApiBase()}/songs/${id}`, { method: "DELETE" }); } + +export async function updateSong(id: string, patch: UpdateSongRequest): Promise { + const res = await fetch(`${getApiBase()}/songs/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error((data as { error?: string }).error ?? `HTTP ${res.status}`); + } + return res.json(); +} diff --git a/app/app/lib/types.ts b/app/app/lib/types.ts index 3498231..bb25c19 100644 --- a/app/app/lib/types.ts +++ b/app/app/lib/types.ts @@ -40,3 +40,9 @@ export interface StoredSong { id: string; song: Song; } + +export interface UpdateSongRequest { + title?: string; + artist?: string; + original_key?: string; +} diff --git a/app/app/routes/home.tsx b/app/app/routes/home.tsx index 8261a76..af0052f 100644 --- a/app/app/routes/home.tsx +++ b/app/app/routes/home.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; +import { useSearchParams, useRevalidator } from "react-router"; import type { Route } from "./+types/home"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; @@ -16,32 +17,38 @@ export function meta({}: Route.MetaArgs) { ]; } -export async function loader() { +export async function loader({ request }: Route.LoaderArgs) { + const q = new URL(request.url).searchParams.get("q") ?? ""; try { - const songs = await listSongs(); - return { songs }; + const songs = await listSongs(q); + return { songs, q, error: false }; } catch { - return { songs: [] }; + return { songs: [], q, error: true }; } } export default function Home({ loaderData }: Route.ComponentProps) { - const { songs } = loaderData; - const [query, setQuery] = useState(""); + const { songs, q: initialQ, error } = loaderData; + const [searchParams, setSearchParams] = useSearchParams(); const [sheetOpen, setSheetOpen] = useState(false); const [localSongs, setLocalSongs] = useState([]); + const revalidator = useRevalidator(); + + const [inputValue, setInputValue] = useState(initialQ); + const debounceRef = useRef | null>(null); + + const handleSearch = useCallback((value: string) => { + setInputValue(value); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + setSearchParams(value.trim() ? { q: value.trim() } : {}, { replace: true }); + }, 300); + }, [setSearchParams]); const allSongs = [...songs, ...localSongs]; - const filtered = query.trim() - ? allSongs.filter( - (s) => - s.meta.title.toLowerCase().includes(query.toLowerCase()) || - s.meta.artist.toLowerCase().includes(query.toLowerCase()) - ) - : allSongs; return ( -
+
{/* Header */}

PocketChords

@@ -55,24 +62,39 @@ export default function Home({ loaderData }: Route.ComponentProps) {
setQuery(e.target.value)} + value={inputValue} + onChange={(e) => handleSearch(e.target.value)} className="w-full" />
+ {/* Error state */} + {error && ( +
+

+ Couldn't load your songs. Is the API running? +

+ +
+ )} + {/* Grid */}
- {filtered.length === 0 && ( + {!error && allSongs.length === 0 && (

- {query ? "No songs match your search." : "No songs yet. Tap Add to get started."} + {initialQ ? "No songs match your search." : "No songs yet. Tap Add to get started."}

)}
- {filtered.map((song) => ( + {allSongs.map((song) => ( ))} - {/* Add card */} setSheetOpen(true)}