fix(app): meta null guard, deduplicate previewChords, env API URL, always show add card

This commit is contained in:
2026-04-08 02:36:09 +02:00
parent 9b68069151
commit 90833c5b93
5 changed files with 24 additions and 39 deletions

1
app/.env.example Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://localhost:8000

View File

@@ -9,6 +9,7 @@ import {
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import type { Song, SongSummary } from "~/lib/types"; import type { Song, SongSummary } from "~/lib/types";
import { previewChords } from "~/lib/mock";
interface Props { interface Props {
open: boolean; open: boolean;
@@ -16,23 +17,6 @@ interface Props {
onSongAdded: (summary: SongSummary) => void; onSongAdded: (summary: SongSummary) => void;
} }
function previewChords(song: Song): string[] {
const seen = new Set<string>();
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) { export function AddSongSheet({ open, onOpenChange, onSongAdded }: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
@@ -58,7 +42,8 @@ export function AddSongSheet({ open, onOpenChange, onSongAdded }: Props) {
throw new Error("File upload requires running the app locally with direct file paths. Use a URL instead."); 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", { const apiBase = import.meta.env.VITE_API_URL ?? "http://localhost:8000";
const resp = await fetch(`${apiBase}/tabs/parse`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source: url.trim() }), body: JSON.stringify({ source: url.trim() }),

View File

@@ -114,7 +114,7 @@ const SONGS_MAP: Record<string, Song> = {
"song-naked": NAKED, "song-naked": NAKED,
}; };
function previewChords(song: Song): string[] { export function previewChords(song: Song): string[] {
const seen = new Set<string>(); const seen = new Set<string>();
const result: string[] = []; const result: string[] = [];
for (const section of song.sections) { for (const section of song.sections) {

View File

@@ -58,11 +58,11 @@ export default function Home({ loaderData }: Route.ComponentProps) {
{/* Grid */} {/* Grid */}
<div className="flex-1 overflow-y-auto px-4 pb-4"> <div className="flex-1 overflow-y-auto px-4 pb-4">
{filtered.length === 0 ? ( {filtered.length === 0 && (
<p className="text-sm text-muted-foreground text-center pt-12"> <p className="text-sm text-muted-foreground text-center pt-8 pb-4">
{query ? "No songs match your search." : "No songs yet. Tap Add to get started."} {query ? "No songs match your search." : "No songs yet. Tap Add to get started."}
</p> </p>
) : ( )}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{filtered.map((song) => ( {filtered.map((song) => (
<SongCard key={song.id} song={song} /> <SongCard key={song.id} song={song} />
@@ -77,7 +77,6 @@ export default function Home({ loaderData }: Route.ComponentProps) {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
)}
</div> </div>
<AddSongSheet <AddSongSheet

View File

@@ -8,10 +8,10 @@ import { getMockSong } from "~/lib/mock";
import type { Song } from "~/lib/types"; import type { Song } from "~/lib/types";
export function meta({ data }: Route.MetaArgs) { export function meta({ data }: Route.MetaArgs) {
if (!data) return [{ title: "Song not found" }]; if (!data?.song) return [{ title: "PocketChords" }];
return [ return [
{ title: `${data.song.meta.title} — PocketChords` }, { title: `${data.song.meta.title} — PocketChords` },
{ name: "description", content: `${data.song.meta.artist}` }, { name: "description", content: data.song.meta.artist },
]; ];
} }
@@ -34,7 +34,7 @@ export default function SongDetail({ loaderData }: Route.ComponentProps) {
const { tempId } = loaderData; const { tempId } = loaderData;
// Resolve song — either from loader or from sessionStorage (temp songs) // Resolve song — either from loader or from sessionStorage (temp songs)
const [song] = useState<Song>(() => { const [song] = useState<Song | null>(() => {
if (loaderData.song) return loaderData.song; if (loaderData.song) return loaderData.song;
if (tempId && typeof window !== "undefined") { if (tempId && typeof window !== "undefined") {
const stored = sessionStorage.getItem(tempId); const stored = sessionStorage.getItem(tempId);