fix(app): meta null guard, deduplicate previewChords, env API URL, always show add card
This commit is contained in:
1
app/.env.example
Normal file
1
app/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
@@ -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() }),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -58,26 +58,25 @@ 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">
|
|
||||||
{filtered.map((song) => (
|
|
||||||
<SongCard key={song.id} song={song} />
|
|
||||||
))}
|
|
||||||
{/* Add card */}
|
|
||||||
<Card
|
|
||||||
className="h-full border-dashed cursor-pointer hover:bg-accent transition-colors"
|
|
||||||
onClick={() => setSheetOpen(true)}
|
|
||||||
>
|
|
||||||
<CardContent className="p-3 flex items-center justify-center h-full min-h-[80px]">
|
|
||||||
<Plus className="w-6 h-6 text-muted-foreground" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{filtered.map((song) => (
|
||||||
|
<SongCard key={song.id} song={song} />
|
||||||
|
))}
|
||||||
|
{/* Add card */}
|
||||||
|
<Card
|
||||||
|
className="h-full border-dashed cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
onClick={() => setSheetOpen(true)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-3 flex items-center justify-center h-full min-h-[80px]">
|
||||||
|
<Plus className="w-6 h-6 text-muted-foreground" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddSongSheet
|
<AddSongSheet
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user