diff --git a/config/development.yaml b/config/development.yaml index c6cf9c8..056844e 100644 --- a/config/development.yaml +++ b/config/development.yaml @@ -24,6 +24,22 @@ server: host: http://localhost # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block middlewares: + cors: + enable: true + allow_origins: + - "http://localhost:3000" + allow_credentials: true + max_age: 3600 + allow_headers: + - "Content-Type" + - "Authorization" + allow_methods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + - "OPTIONS" + # Worker Configuration workers: diff --git a/music-metadata-manager-frontend/.gitignore b/music-metadata-manager-frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/music-metadata-manager-frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/music-metadata-manager-frontend/README.md b/music-metadata-manager-frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/music-metadata-manager-frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/music-metadata-manager-frontend/bun.lockb b/music-metadata-manager-frontend/bun.lockb new file mode 100755 index 0000000..dc8e755 Binary files /dev/null and b/music-metadata-manager-frontend/bun.lockb differ diff --git a/music-metadata-manager-frontend/components.json b/music-metadata-manager-frontend/components.json new file mode 100644 index 0000000..b1fa786 --- /dev/null +++ b/music-metadata-manager-frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/music-metadata-manager-frontend/eslint.config.mjs b/music-metadata-manager-frontend/eslint.config.mjs new file mode 100644 index 0000000..c85fb67 --- /dev/null +++ b/music-metadata-manager-frontend/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/music-metadata-manager-frontend/next.config.ts b/music-metadata-manager-frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/music-metadata-manager-frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/music-metadata-manager-frontend/package.json b/music-metadata-manager-frontend/package.json new file mode 100644 index 0000000..ae01444 --- /dev/null +++ b/music-metadata-manager-frontend/package.json @@ -0,0 +1,71 @@ +{ + "name": "music-metadata-manager-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.525.0", + "next": "15.4.4", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-day-picker": "^9.8.0", + "react-dom": "19.1.0", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^3.0.3", + "recharts": "2.15.4", + "sonner": "^2.0.6", + "tailwind-merge": "^3.3.1", + "tw-animate-css": "^1.3.6", + "vaul": "^1.1.2", + "zod": "^4.0.10" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@tailwindcss/postcss": "^4", + "tailwindcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.4.4", + "@eslint/eslintrc": "^3" + } +} diff --git a/music-metadata-manager-frontend/postcss.config.mjs b/music-metadata-manager-frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/music-metadata-manager-frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/music-metadata-manager-frontend/public/file.svg b/music-metadata-manager-frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/music-metadata-manager-frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-metadata-manager-frontend/public/globe.svg b/music-metadata-manager-frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/music-metadata-manager-frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-metadata-manager-frontend/public/next.svg b/music-metadata-manager-frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/music-metadata-manager-frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-metadata-manager-frontend/public/vercel.svg b/music-metadata-manager-frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/music-metadata-manager-frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-metadata-manager-frontend/public/window.svg b/music-metadata-manager-frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/music-metadata-manager-frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-metadata-manager-frontend/src/app/favicon.ico b/music-metadata-manager-frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/music-metadata-manager-frontend/src/app/favicon.ico differ diff --git a/music-metadata-manager-frontend/src/app/globals.css b/music-metadata-manager-frontend/src/app/globals.css new file mode 100644 index 0000000..0ad90d5 --- /dev/null +++ b/music-metadata-manager-frontend/src/app/globals.css @@ -0,0 +1,123 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/music-metadata-manager-frontend/src/app/layout.tsx b/music-metadata-manager-frontend/src/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/music-metadata-manager-frontend/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/music-metadata-manager-frontend/src/app/page.tsx b/music-metadata-manager-frontend/src/app/page.tsx new file mode 100644 index 0000000..f271b2b --- /dev/null +++ b/music-metadata-manager-frontend/src/app/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { AddLibraryForm } from "@/components/add-library-form"; +import { LoginForm } from "@/components/login-form"; +import MusicFilesPage from "@/components/music-files"; +import { Toaster } from "@/components/ui/sonner"; + +export default function Home() { + return ( +
+ + + ; + ; +
+ ); +} diff --git a/music-metadata-manager-frontend/src/components/add-library-form.tsx b/music-metadata-manager-frontend/src/components/add-library-form.tsx new file mode 100644 index 0000000..30d13f1 --- /dev/null +++ b/music-metadata-manager-frontend/src/components/add-library-form.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; +import { fetchFromApi } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; + +export function AddLibraryForm() { + const [path, setPath] = useState(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!path) return; + + try { + setLoading(true); + await fetchFromApi("/music_libraries", { + method: "POST", + body: JSON.stringify({ path }), + }); + toast.success("Library added!"); + setPath(""); + } catch (err: any) { + console.error(err); + toast.error("Failed to add library"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + setPath(e.target.value)} + placeholder="/home/username/Music" + /> +
+ +
+ ); +} diff --git a/music-metadata-manager-frontend/src/components/login-form.tsx b/music-metadata-manager-frontend/src/components/login-form.tsx new file mode 100644 index 0000000..a28539a --- /dev/null +++ b/music-metadata-manager-frontend/src/components/login-form.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; +import { fetchFromApi } from "@/lib/api"; + +type LoginResponse = { + token: string; + pid: string; + name: string; + is_verified: boolean; +}; + +export function LoginForm({ onLogin }: { onLogin?: () => void }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + setLoading(true); + const res = await fetchFromApi("/auth/login", { + method: "POST", + body: JSON.stringify({ email, password }), + }); + + const { token } = res; + if (!token) { + throw new Error("Login failed: No token received"); + } + + localStorage.setItem("auth_token", token); + toast.success("Logged in!"); + if (onLogin) onLogin(); + } catch (err: any) { + console.error(err); + toast.error(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + setEmail(e.target.value)} + placeholder="user@example.com" + /> +
+ +
+ + setPassword(e.target.value)} + /> +
+ + +
+ ); +} diff --git a/music-metadata-manager-frontend/src/components/music-files.tsx b/music-metadata-manager-frontend/src/components/music-files.tsx new file mode 100644 index 0000000..7f0578b --- /dev/null +++ b/music-metadata-manager-frontend/src/components/music-files.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { fetchFromApi } from "@/lib/api"; + +type MusicFile = { + id: number; + title: string; + artist?: string; + album?: string; + path: string; + metadata: object; + music_library_id: number; +}; + +export default function MusicFilesPage() { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchFromApi("/music_files") + .then(setFiles) + .catch((err) => console.error(err)) + .finally(() => setLoading(false)); + }, []); + + if (loading) return
Loading...
; + + return ( +
+

Music Files

+
    + {files.map((file) => ( +
  • +

    + {file.title} +

    +

    + {file.artist} – {file.album} +

    +

    {file.path}

    +
  • + ))} +
+
+ ); +} diff --git a/music-metadata-manager-frontend/src/components/ui/accordion.tsx b/music-metadata-manager-frontend/src/components/ui/accordion.tsx new file mode 100644 index 0000000..d21b65f --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/music-metadata-manager-frontend/src/components/ui/alert-dialog.tsx b/music-metadata-manager-frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0863e40 --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/music-metadata-manager-frontend/src/components/ui/alert.tsx b/music-metadata-manager-frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/music-metadata-manager-frontend/src/components/ui/aspect-ratio.tsx b/music-metadata-manager-frontend/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..9b491fb --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,9 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/music-metadata-manager-frontend/src/components/ui/avatar.tsx b/music-metadata-manager-frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/music-metadata-manager-frontend/src/components/ui/badge.tsx b/music-metadata-manager-frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/music-metadata-manager-frontend/src/components/ui/breadcrumb.tsx b/music-metadata-manager-frontend/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/music-metadata-manager-frontend/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return