From 607d0b112e4db5592cbf429aefcb5c0cec0a9c8a Mon Sep 17 00:00:00 2001 From: BusyBee3333 Date: Mon, 10 Nov 2025 16:13:01 -0500 Subject: [PATCH] Add Next.js frontend scaffold --- frontend/app/globals.css | 12 +++++ frontend/app/layout.tsx | 44 ++++++++++++++++ frontend/app/page.tsx | 18 +++++++ frontend/app/projects/[id]/page.tsx | 57 ++++++++++++++++++++ frontend/app/projects/page.tsx | 60 +++++++++++++++++++++ frontend/components/AudioUpload.tsx | 55 ++++++++++++++++++++ frontend/components/ClipGrid.tsx | 52 +++++++++++++++++++ frontend/components/LyricsEditor.tsx | 65 +++++++++++++++++++++++ frontend/components/providers.tsx | 14 +++++ frontend/lib/api.ts | 78 ++++++++++++++++++++++++++++ frontend/next-env.d.ts | 5 ++ frontend/next.config.mjs | 8 +++ frontend/package.json | 28 ++++++++++ frontend/postcss.config.mjs | 6 +++ frontend/tailwind.config.ts | 14 +++++ frontend/tsconfig.json | 21 ++++++++ 16 files changed, 537 insertions(+) create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/app/projects/[id]/page.tsx create mode 100644 frontend/app/projects/page.tsx create mode 100644 frontend/components/AudioUpload.tsx create mode 100644 frontend/components/ClipGrid.tsx create mode 100644 frontend/components/LyricsEditor.tsx create mode 100644 frontend/components/providers.tsx create mode 100644 frontend/lib/api.ts create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..8295d3d --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: light; +} + +body { + min-height: 100vh; + background-color: #f8fafc; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..e6d1bfa --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,44 @@ +import './globals.css'; +import type { Metadata } from 'next'; +import { Providers } from '../components/providers'; +import Link from 'next/link'; + +export const metadata: Metadata = { + title: 'Beatmatchr', + description: 'Frontend for Beatmatchr project editor' +}; + +export default function RootLayout({ + children +}: { + children: React.ReactNode; +}) { + return ( + + + +
+
+
+ + Beatmatchr + + +
+
+
+
{children}
+
+
+
+ + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..bf7dc81 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,18 @@ +import Link from 'next/link'; + +export default function HomePage() { + return ( +
+

Welcome to Beatmatchr

+

+ Manage your audio projects, organize source clips, and fine-tune lyrics in a clean, focused editor. +

+ + Create Project + +
+ ); +} diff --git a/frontend/app/projects/[id]/page.tsx b/frontend/app/projects/[id]/page.tsx new file mode 100644 index 0000000..82a4f0d --- /dev/null +++ b/frontend/app/projects/[id]/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { getProject, Project } from '../../../lib/api'; +import { AudioUpload } from '../../../components/AudioUpload'; +import { ClipGrid } from '../../../components/ClipGrid'; +import { LyricsEditor } from '../../../components/LyricsEditor'; + +export default function ProjectDetailPage() { + const params = useParams<{ id: string }>(); + const projectId = params?.id; + const queryClient = useQueryClient(); + + const { + data: project, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['projects', projectId], + queryFn: () => getProject(projectId as string), + enabled: Boolean(projectId) + }); + + if (!projectId) { + return

Project ID is missing.

; + } + + return ( +
+ {isLoading &&

Loading project...

} + {isError &&

{error.message}

} + {project && ( + <> +
+

{project.name}

+ {project.description &&

{project.description}

} +
+
+ + queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'source-clips'] }) + } + /> + +
+
+

Source Clips

+ +
+ + )} +
+ ); +} diff --git a/frontend/app/projects/page.tsx b/frontend/app/projects/page.tsx new file mode 100644 index 0000000..d8dbafb --- /dev/null +++ b/frontend/app/projects/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { getProjects, Project } from '../../lib/api'; + +export default function ProjectsPage() { + const { + data: projects, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['projects'], + queryFn: getProjects + }); + + return ( +
+
+
+

Projects

+

Browse and open your existing audio projects.

+
+ + New Project + +
+ + {isLoading &&

Loading projects...

} + {isError &&

{error.message}

} + + {projects && projects.length === 0 && ( +
+ No projects yet. Create one from the backend or CLI. +
+ )} + + {projects && projects.length > 0 && ( +
    + {projects.map((project) => ( +
  • +

    {project.name}

    + {project.description &&

    {project.description}

    } +
    + {project.created_at && Created {new Date(project.created_at).toLocaleDateString()}} + + Open + +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/frontend/components/AudioUpload.tsx b/frontend/components/AudioUpload.tsx new file mode 100644 index 0000000..4ed75d4 --- /dev/null +++ b/frontend/components/AudioUpload.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useMutation } from '@tanstack/react-query'; +import { FormEvent, useState } from 'react'; +import { uploadProjectAudio } from '../lib/api'; + +interface AudioUploadProps { + projectId: string; + onSuccess?: () => void; +} + +export function AudioUpload({ projectId, onSuccess }: AudioUploadProps) { + const [file, setFile] = useState(null); + + const mutation = useMutation({ + mutationFn: async () => { + if (!file) { + throw new Error('Please select an audio file to upload.'); + } + await uploadProjectAudio(projectId, file); + }, + onSuccess + }); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + mutation.mutate(); + }; + + return ( +
+
+

Upload Audio

+

Attach a new audio track to this project.

+
+ setFile(event.target.files?.[0] ?? null)} + className="block w-full rounded-md border border-slate-300 px-3 py-2 text-sm" + /> + + {mutation.isError && ( +

{(mutation.error as Error).message}

+ )} + {mutation.isSuccess &&

Audio uploaded successfully!

} +
+ ); +} diff --git a/frontend/components/ClipGrid.tsx b/frontend/components/ClipGrid.tsx new file mode 100644 index 0000000..cbc6aab --- /dev/null +++ b/frontend/components/ClipGrid.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; +import { getSourceClips, SourceClip } from '../lib/api'; + +interface ClipGridProps { + projectId: string; +} + +export function ClipGrid({ projectId }: ClipGridProps) { + const { + data: clips, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['projects', projectId, 'source-clips'], + queryFn: () => getSourceClips(projectId) + }); + + if (isLoading) { + return

Loading source clips...

; + } + + if (isError) { + return

{error.message}

; + } + + if (!clips || clips.length === 0) { + return ( +
+ No source clips found yet. +
+ ); + } + + return ( +
+ {clips.map((clip) => ( +
+

{clip.name}

+ {clip.duration != null && ( +

Duration: {clip.duration.toFixed(1)}s

+ )} + {clip.waveform_url && ( +

Waveform: {clip.waveform_url}

+ )} +
+ ))} +
+ ); +} diff --git a/frontend/components/LyricsEditor.tsx b/frontend/components/LyricsEditor.tsx new file mode 100644 index 0000000..0ca981a --- /dev/null +++ b/frontend/components/LyricsEditor.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { getLyrics, Lyrics, updateLyrics } from '../lib/api'; +import { useEffect, useState } from 'react'; + +interface LyricsEditorProps { + projectId: string; +} + +export function LyricsEditor({ projectId }: LyricsEditorProps) { + const queryClient = useQueryClient(); + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['projects', projectId, 'lyrics'], + queryFn: () => getLyrics(projectId) + }); + + const [text, setText] = useState(''); + + useEffect(() => { + if (data) { + setText(data.raw_text ?? ''); + } + }, [data]); + + const mutation = useMutation({ + mutationFn: () => updateLyrics(projectId, { raw_text: text }), + onSuccess: (updated) => { + queryClient.setQueryData(['projects', projectId, 'lyrics'], updated); + } + }); + + return ( +
+
+

Lyrics

+

Review and edit the current lyrics.

+
+ {isLoading &&

Loading lyrics...

} + {isError &&

{error.message}

} + {!isLoading && !isError && ( + <> +