Ethos track metadata form component (genre, BPM, license type)

cgen-7227aa2e467e40459b7606b188834487
This commit is contained in:
Builder.io 2025-11-11 23:09:44 +00:00
parent 851c390bed
commit 94cb755156
2 changed files with 365 additions and 0 deletions

View file

@ -0,0 +1,188 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
interface TrackMetadata {
title: string;
description?: string;
genre: string[];
bpm?: number;
license_type: "ecosystem" | "commercial_sample";
is_published: boolean;
}
interface TrackMetadataFormProps {
onSubmit: (metadata: TrackMetadata) => void;
initialData?: Partial<TrackMetadata>;
isLoading?: boolean;
}
const GENRES = [
"Synthwave",
"Orchestral",
"SFX",
"Ambient",
"Electronic",
"Cinematic",
"Jazz",
"Hip-Hop",
"Folk",
];
export default function TrackMetadataForm({
onSubmit,
initialData,
isLoading,
}: TrackMetadataFormProps) {
const [title, setTitle] = useState(initialData?.title || "");
const [description, setDescription] = useState(initialData?.description || "");
const [selectedGenres, setSelectedGenres] = useState<string[]>(
initialData?.genre || [],
);
const [bpm, setBpm] = useState(initialData?.bpm || "");
const [licenseType, setLicenseType] = useState<"ecosystem" | "commercial_sample">(
initialData?.license_type || "ecosystem",
);
const [isPublished, setIsPublished] = useState(
initialData?.is_published !== false,
);
const toggleGenre = (genre: string) => {
setSelectedGenres((prev) =>
prev.includes(genre)
? prev.filter((g) => g !== genre)
: [...prev, genre],
);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title || selectedGenres.length === 0) {
alert("Title and at least one genre are required");
return;
}
onSubmit({
title,
description: description || undefined,
genre: selectedGenres,
bpm: bpm ? Number(bpm) : undefined,
license_type: licenseType,
is_published: isPublished,
});
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="title" className="text-white">
Track Title *
</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g., Synthwave Dream"
className="bg-slate-800 border-slate-700"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description" className="text-white">
Description
</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Tell artists and composers about your track..."
className="bg-slate-800 border-slate-700 h-24"
/>
</div>
<div className="space-y-3">
<Label className="text-white">Genres * (select at least one)</Label>
<div className="grid grid-cols-2 gap-3">
{GENRES.map((genre) => (
<label key={genre} className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={selectedGenres.includes(genre)}
onCheckedChange={() => toggleGenre(genre)}
className="border-slate-600"
/>
<span className="text-sm text-slate-300">{genre}</span>
</label>
))}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="bpm" className="text-white">
BPM (optional)
</Label>
<Input
id="bpm"
type="number"
value={bpm}
onChange={(e) => setBpm(e.target.value)}
placeholder="120"
className="bg-slate-800 border-slate-700"
min="30"
max="300"
/>
</div>
<div className="space-y-2">
<Label htmlFor="license" className="text-white">
License Type *
</Label>
<Select value={licenseType} onValueChange={(val: any) => setLicenseType(val)}>
<SelectTrigger className="bg-slate-800 border-slate-700">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="ecosystem">
Ecosystem (Non-commercial)
</SelectItem>
<SelectItem value="commercial_sample">
Commercial Demo
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<label className="flex items-center gap-2 cursor-pointer p-3 rounded-lg bg-slate-800/50 border border-slate-700">
<Checkbox
checked={isPublished}
onCheckedChange={(checked) => setIsPublished(checked as boolean)}
className="border-slate-600"
/>
<span className="text-sm text-slate-300">
Publish immediately (visible in track library)
</span>
</label>
<Button
type="submit"
disabled={isLoading}
className="w-full bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-700 hover:to-purple-700"
>
{isLoading ? "Saving..." : "Save Track Metadata"}
</Button>
</form>
);
}

View file

@ -0,0 +1,177 @@
import { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { AlertCircle, Upload, CheckCircle2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
interface TrackUploadModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onFileSelected: (file: File) => void;
isLoading?: boolean;
}
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
const ALLOWED_TYPES = ["audio/mpeg", "audio/wav", "audio/mp3"];
export default function TrackUploadModal({
open,
onOpenChange,
onFileSelected,
isLoading,
}: TrackUploadModalProps) {
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState<string | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (!selectedFile) return;
setError(null);
setFile(null);
if (!ALLOWED_TYPES.includes(selectedFile.type)) {
setError("Please upload an MP3 or WAV file");
return;
}
if (selectedFile.size > MAX_FILE_SIZE) {
setError("File is too large. Maximum size is 50MB");
return;
}
setFile(selectedFile);
};
const handleUpload = () => {
if (!file) return;
onFileSelected(file);
};
const handleReset = () => {
setFile(null);
setError(null);
setUploadProgress(0);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleClose = () => {
if (!isLoading) {
handleReset();
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="bg-slate-900 border-slate-700">
<DialogHeader>
<DialogTitle className="text-white">Upload Audio Track</DialogTitle>
<DialogDescription className="text-slate-400">
Upload your music or sound effects (MP3 or WAV, up to 50MB)
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{!file ? (
<div
className="relative border-2 border-dashed border-slate-600 rounded-lg p-8 text-center cursor-pointer hover:border-slate-500 transition"
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept="audio/mpeg,audio/wav,audio/mp3"
onChange={handleFileChange}
className="hidden"
/>
<Upload className="h-8 w-8 mx-auto mb-2 text-slate-500" />
<p className="text-sm font-medium text-white mb-1">
Click to upload or drag and drop
</p>
<p className="text-xs text-slate-500">MP3 or WAV Up to 50MB</p>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700">
<CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" />
<div className="text-sm">
<p className="text-white font-medium">{file.name}</p>
<p className="text-slate-400 text-xs">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</div>
{isLoading && (
<div className="space-y-2">
<div className="flex justify-between text-xs text-slate-400">
<span>Uploading...</span>
<span>{uploadProgress}%</span>
</div>
<Progress value={uploadProgress} className="h-2" />
</div>
)}
</div>
)}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-500 flex-shrink-0" />
<p className="text-sm text-red-400">{error}</p>
</div>
)}
<div className="flex gap-3">
<Button
variant="outline"
onClick={handleClose}
disabled={isLoading}
className="flex-1 border-slate-700"
>
Cancel
</Button>
{file && !isLoading && (
<>
<Button
variant="outline"
onClick={handleReset}
className="flex-1 border-slate-700"
>
Choose Different
</Button>
<Button
onClick={handleUpload}
className="flex-1 bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-700 hover:to-purple-700"
>
Proceed to Details
</Button>
</>
)}
{!file && (
<Button
onClick={() => fileInputRef.current?.click()}
className="flex-1 bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-700 hover:to-purple-700"
>
Browse Files
</Button>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}