256 lines
9.2 KiB
React
256 lines
9.2 KiB
React
|
|
import { useState } from "react";
|
||
|
|
import { useNavigate } from "react-router-dom";
|
||
|
|
import { motion, AnimatePresence } from "framer-motion";
|
||
|
|
import {
|
||
|
|
Upload,
|
||
|
|
Plus,
|
||
|
|
FileText,
|
||
|
|
Music,
|
||
|
|
X,
|
||
|
|
Check,
|
||
|
|
AlertCircle,
|
||
|
|
} from "lucide-react";
|
||
|
|
import toast from "react-hot-toast";
|
||
|
|
|
||
|
|
function QuickActions() {
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||
|
|
const [dragActive, setDragActive] = useState(false);
|
||
|
|
const [uploading, setUploading] = useState(false);
|
||
|
|
const [uploadedFile, setUploadedFile] = useState(null);
|
||
|
|
|
||
|
|
const handleDrag = (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
if (e.type === "dragenter" || e.type === "dragover") {
|
||
|
|
setDragActive(true);
|
||
|
|
} else if (e.type === "dragleave") {
|
||
|
|
setDragActive(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDrop = (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
setDragActive(false);
|
||
|
|
|
||
|
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||
|
|
handleFile(e.dataTransfer.files[0]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleFileInput = (e) => {
|
||
|
|
if (e.target.files && e.target.files[0]) {
|
||
|
|
handleFile(e.target.files[0]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleFile = async (file) => {
|
||
|
|
// Check file type
|
||
|
|
const validTypes = ["text/plain", "application/pdf", ".docx", ".doc"];
|
||
|
|
const isValid = validTypes.some(
|
||
|
|
(type) => file.type.includes(type) || file.name.endsWith(type),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (
|
||
|
|
!isValid &&
|
||
|
|
!file.name.endsWith(".txt") &&
|
||
|
|
!file.name.endsWith(".pdf")
|
||
|
|
) {
|
||
|
|
toast.error("Please upload a .txt, .pdf, or .docx file");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setUploadedFile(file);
|
||
|
|
setUploading(true);
|
||
|
|
|
||
|
|
// Simulate upload
|
||
|
|
await new Promise((r) => setTimeout(r, 1500));
|
||
|
|
setUploading(false);
|
||
|
|
toast.success("File uploaded successfully!");
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleProcessFile = () => {
|
||
|
|
// Navigate to song editor with parsed content
|
||
|
|
setShowUploadModal(false);
|
||
|
|
setUploadedFile(null);
|
||
|
|
navigate("/song/new");
|
||
|
|
toast.success("Creating new song from lyrics...");
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
|
|
{/* Upload Lyrics Tile */}
|
||
|
|
<motion.div
|
||
|
|
whileHover={{ scale: 1.02, y: -4 }}
|
||
|
|
whileTap={{ scale: 0.98 }}
|
||
|
|
onClick={() => setShowUploadModal(true)}
|
||
|
|
className="glass-card p-6 cursor-pointer group overflow-hidden relative"
|
||
|
|
>
|
||
|
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/10 to-pink-500/10 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||
|
|
<div className="relative">
|
||
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-400 to-pink-500 flex items-center justify-center mb-4 shadow-soft group-hover:shadow-lg transition-shadow">
|
||
|
|
<Upload className="w-7 h-7 text-white" />
|
||
|
|
</div>
|
||
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-1">
|
||
|
|
Upload Lyrics
|
||
|
|
</h3>
|
||
|
|
<p className="text-gray-500 text-sm">
|
||
|
|
Import lyrics from .txt, .pdf, or .docx files
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
{/* Create New Song Tile */}
|
||
|
|
<motion.div
|
||
|
|
whileHover={{ scale: 1.02, y: -4 }}
|
||
|
|
whileTap={{ scale: 0.98 }}
|
||
|
|
onClick={() => navigate("/song/new")}
|
||
|
|
className="glass-card p-6 cursor-pointer group overflow-hidden relative"
|
||
|
|
>
|
||
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-cyan-500/10 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||
|
|
<div className="relative">
|
||
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-400 to-cyan-500 flex items-center justify-center mb-4 shadow-soft group-hover:shadow-lg transition-shadow">
|
||
|
|
<Plus className="w-7 h-7 text-white" />
|
||
|
|
</div>
|
||
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-1">
|
||
|
|
Create New Song
|
||
|
|
</h3>
|
||
|
|
<p className="text-gray-500 text-sm">
|
||
|
|
Start from scratch with our chord editor
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Upload Modal */}
|
||
|
|
<AnimatePresence>
|
||
|
|
{showUploadModal && (
|
||
|
|
<>
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
exit={{ opacity: 0 }}
|
||
|
|
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50"
|
||
|
|
onClick={() => {
|
||
|
|
setShowUploadModal(false);
|
||
|
|
setUploadedFile(null);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
||
|
|
animate={{ opacity: 1, scale: 1 }}
|
||
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
||
|
|
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg glass-card p-6 z-50"
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<h2 className="text-xl font-bold text-gray-800">
|
||
|
|
Upload Lyrics
|
||
|
|
</h2>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setShowUploadModal(false);
|
||
|
|
setUploadedFile(null);
|
||
|
|
}}
|
||
|
|
className="p-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||
|
|
>
|
||
|
|
<X className="w-5 h-5 text-gray-500" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{!uploadedFile ? (
|
||
|
|
<div
|
||
|
|
onDragEnter={handleDrag}
|
||
|
|
onDragLeave={handleDrag}
|
||
|
|
onDragOver={handleDrag}
|
||
|
|
onDrop={handleDrop}
|
||
|
|
className={`border-2 border-dashed rounded-2xl p-8 text-center transition-colors ${
|
||
|
|
dragActive
|
||
|
|
? "border-primary-500 bg-primary-50"
|
||
|
|
: "border-gray-300 hover:border-gray-400"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-100 flex items-center justify-center">
|
||
|
|
<FileText className="w-8 h-8 text-gray-400" />
|
||
|
|
</div>
|
||
|
|
<p className="text-gray-600 mb-2">
|
||
|
|
Drag and drop your file here, or{" "}
|
||
|
|
<label className="text-primary-600 font-medium cursor-pointer hover:underline">
|
||
|
|
browse
|
||
|
|
<input
|
||
|
|
type="file"
|
||
|
|
accept=".txt,.pdf,.doc,.docx"
|
||
|
|
onChange={handleFileInput}
|
||
|
|
className="hidden"
|
||
|
|
/>
|
||
|
|
</label>
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-gray-400">
|
||
|
|
Supports .txt, .pdf, .docx files
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="flex items-center gap-4 p-4 bg-gray-50 rounded-xl">
|
||
|
|
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
|
||
|
|
{uploading ? (
|
||
|
|
<div className="w-5 h-5 border-2 border-green-600 border-t-transparent rounded-full animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Check className="w-6 h-6 text-green-600" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<p className="font-medium text-gray-800 truncate">
|
||
|
|
{uploadedFile.name}
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-gray-500">
|
||
|
|
{uploading ? "Uploading..." : "Ready to process"}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
{!uploading && (
|
||
|
|
<button
|
||
|
|
onClick={() => setUploadedFile(null)}
|
||
|
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||
|
|
>
|
||
|
|
<X className="w-4 h-4 text-gray-500" />
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-start gap-3 p-3 bg-blue-50 rounded-xl">
|
||
|
|
<AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||
|
|
<p className="text-sm text-blue-700">
|
||
|
|
We'll try to detect sections (Verse, Chorus, etc.) and
|
||
|
|
existing chord notations automatically.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<button
|
||
|
|
onClick={() => setUploadedFile(null)}
|
||
|
|
className="btn-ghost flex-1"
|
||
|
|
>
|
||
|
|
Upload Different File
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleProcessFile}
|
||
|
|
disabled={uploading}
|
||
|
|
className="btn-primary flex-1"
|
||
|
|
>
|
||
|
|
<Music className="w-4 h-4 mr-2" />
|
||
|
|
Create Song
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</motion.div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default QuickActions;
|