Avatar improvements: - Image loading skeleton with animate-pulse overlay - Fade-in transition on image load (opacity 0→1, 200ms) - Error fallback shows initials when image fails - Click animation: active:scale-95 - Badge overlay prop: count/dot/color at top-right corner Accordion/Collapsible smooth animation: - Replaced max-h-[5000px] hack with CSS grid-template-rows trick - grid-rows-[0fr] → grid-rows-[1fr] for content-aware smooth collapse - 200ms ease-out transition on both Collapsible and AccordionContent Modal animation consistency (5 modals migrated): - CreatePlaylistModal → base Modal (focus trap, AnimatePresence, portal) - AutoMetadataDetectionModal → base Modal - ReviewProductModal → base Modal - ChangeUsernameModal → base Modal - TagSuggestionsModal → base Modal - Skipped: SharePostModal (multi-view pattern, would lose layout flexibility) Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { Modal } from '@/components/ui/modal';
|
|
import { Check, Music2 } from 'lucide-react';
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
|
|
|
interface DetectedData {
|
|
bpm: number;
|
|
key: string;
|
|
genre: string;
|
|
energy: string;
|
|
}
|
|
|
|
interface AutoMetadataDetectionModalProps {
|
|
onClose: () => void;
|
|
onApply: (data: DetectedData) => void;
|
|
fileName: string;
|
|
}
|
|
|
|
export const AutoMetadataDetectionModal: React.FC<
|
|
AutoMetadataDetectionModalProps
|
|
> = ({ onClose, onApply, fileName }) => {
|
|
const { addToast: _addToast } = useToast();
|
|
const [loading, setLoading] = useState(true);
|
|
const [result, setResult] = useState<DetectedData | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Simulate AI detection
|
|
const timer = setTimeout(() => {
|
|
setResult({
|
|
bpm: 128,
|
|
key: 'F# Minor',
|
|
genre: 'Synthwave',
|
|
energy: 'High',
|
|
});
|
|
setLoading(false);
|
|
}, 2500);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
return (
|
|
<Modal
|
|
open={true}
|
|
onClose={onClose}
|
|
title="AI Metadata Detection"
|
|
>
|
|
<div className="flex flex-col items-center text-center">
|
|
{loading ? (
|
|
<div className="space-y-6">
|
|
<div className="relative">
|
|
<div className="w-20 h-20 rounded-full border-4 border-border border-t-kodo-steel animate-spin mx-auto"></div>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<Music2 className="w-8 h-8 text-muted-foreground/50" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-lg font-bold text-white animate-pulse">
|
|
Analyzing Audio...
|
|
</h4>
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
Detecting BPM, Key, and Genre for <br />
|
|
<span className="text-muted-foreground">{fileName}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="w-full space-y-6 animate-fadeIn">
|
|
<div className="bg-card border border-border/20 rounded-lg p-6 w-full">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="text-center p-2">
|
|
<div className="text-xs text-muted-foreground uppercase font-bold mb-1">
|
|
Detected BPM
|
|
</div>
|
|
<div className="text-2xl font-bold text-white">
|
|
{result?.bpm}
|
|
</div>
|
|
</div>
|
|
<div className="text-center p-2">
|
|
<div className="text-xs text-muted-foreground uppercase font-bold mb-1">
|
|
Detected Key
|
|
</div>
|
|
<div className="text-2xl font-bold text-muted-foreground">
|
|
{result?.key}
|
|
</div>
|
|
</div>
|
|
<div className="text-center p-2">
|
|
<div className="text-xs text-muted-foreground uppercase font-bold mb-1">
|
|
Genre
|
|
</div>
|
|
<div className="text-lg font-medium text-white">
|
|
{result?.genre}
|
|
</div>
|
|
</div>
|
|
<div className="text-center p-2">
|
|
<div className="text-xs text-muted-foreground uppercase font-bold mb-1">
|
|
Energy Level
|
|
</div>
|
|
<div className="text-lg font-medium text-warning">
|
|
{result?.energy}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4 w-full">
|
|
<Button variant="ghost" onClick={onClose} className="flex-1">
|
|
Discard
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
className="flex-1"
|
|
icon={<Check className="w-4 h-4" />}
|
|
onClick={() => result && onApply(result)}
|
|
>
|
|
Apply Tags
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|