| // src/components/project/TaskModal.tsx | |
| import { useState } from "react"; | |
| import { X, Calendar, Flag, User, Loader2, CheckSquare } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import api from "@/lib/api"; | |
| import { toast } from "react-toastify"; | |
| interface TaskModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| projectId: number; | |
| onSuccess: () => void; | |
| } | |
| const PRIORITIES = [ | |
| { value: "low", label: "Low", color: "text-emerald-600 bg-emerald-50 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400" }, | |
| { value: "medium", label: "Medium", color: "text-amber-600 bg-amber-50 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400" }, | |
| { value: "high", label: "High", color: "text-red-600 bg-red-50 border-red-200 dark:bg-red-900/20 dark:text-red-400" }, | |
| ]; | |
| export function TaskModal({ isOpen, onClose, projectId, onSuccess }: TaskModalProps) { | |
| const [title, setTitle] = useState(""); | |
| const [description, setDescription] = useState(""); | |
| const [priority, setPriority] = useState<"low" | "medium" | "high">("medium"); | |
| const [dueDate, setDueDate] = useState(""); | |
| const [saving, setSaving] = useState(false); | |
| const resetForm = () => { | |
| setTitle(""); | |
| setDescription(""); | |
| setPriority("medium"); | |
| setDueDate(""); | |
| }; | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!title.trim()) { | |
| toast.error("Task title is required"); | |
| return; | |
| } | |
| setSaving(true); | |
| try { | |
| await api.post(`/projects/${projectId}/tasks`, { | |
| title: title.trim(), | |
| description: description.trim(), | |
| priority, | |
| dueDate: dueDate ? new Date(dueDate).toISOString() : null, | |
| }); | |
| toast.success("✅ Task created successfully!"); | |
| resetForm(); | |
| onSuccess(); | |
| onClose(); | |
| } catch (err: any) { | |
| toast.error(err.response?.data?.error || "Failed to create task"); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const handleClose = () => { | |
| resetForm(); | |
| onClose(); | |
| }; | |
| return ( | |
| <AnimatePresence> | |
| {isOpen && ( | |
| <> | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50" | |
| onClick={handleClose} | |
| /> | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.92, y: 20 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.92, y: 20 }} | |
| transition={{ type: "spring", stiffness: 300, damping: 25 }} | |
| className="w-full max-w-md bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/30 dark:border-gray-700/50 overflow-hidden" | |
| > | |
| {/* Header */} | |
| <div className="flex items-center justify-between px-6 py-5 border-b border-gray-100 dark:border-gray-800"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center"> | |
| <CheckSquare className="w-5 h-5 text-primary" /> | |
| </div> | |
| <h2 className="text-lg font-bold text-gray-900 dark:text-white">Create New Task</h2> | |
| </div> | |
| <button | |
| onClick={handleClose} | |
| className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| {/* Form */} | |
| <form onSubmit={handleSubmit} className="p-6 space-y-5"> | |
| {/* Title */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> | |
| Task Title <span className="text-red-500">*</span> | |
| </label> | |
| <input | |
| type="text" | |
| value={title} | |
| onChange={e => setTitle(e.target.value)} | |
| className="w-full px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition placeholder-gray-400" | |
| placeholder="What needs to be done?" | |
| autoFocus | |
| /> | |
| </div> | |
| {/* Description */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> | |
| Description <span className="text-gray-400 font-normal">(optional)</span> | |
| </label> | |
| <textarea | |
| value={description} | |
| onChange={e => setDescription(e.target.value)} | |
| rows={3} | |
| className="w-full px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition resize-none placeholder-gray-400" | |
| placeholder="Add more details about this task..." | |
| /> | |
| </div> | |
| {/* Priority */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> | |
| <Flag className="w-3.5 h-3.5 inline mr-1" /> | |
| Priority | |
| </label> | |
| <div className="grid grid-cols-3 gap-2"> | |
| {PRIORITIES.map(p => ( | |
| <button | |
| key={p.value} | |
| type="button" | |
| onClick={() => setPriority(p.value as any)} | |
| className={`py-2.5 rounded-xl border text-sm font-medium transition-all ${ | |
| priority === p.value ? p.color + " ring-1 ring-offset-1 ring-current" : "border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:border-gray-300" | |
| }`} | |
| > | |
| {p.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Due Date */} | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> | |
| <Calendar className="w-3.5 h-3.5 inline mr-1" /> | |
| Due Date <span className="text-gray-400 font-normal">(optional)</span> | |
| </label> | |
| <input | |
| type="date" | |
| value={dueDate} | |
| onChange={e => setDueDate(e.target.value)} | |
| min={new Date().toISOString().split("T")[0]} | |
| className="w-full px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition" | |
| /> | |
| </div> | |
| {/* Actions */} | |
| <div className="flex gap-3 pt-1"> | |
| <button | |
| type="button" | |
| onClick={handleClose} | |
| className="flex-1 px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-800 transition" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| disabled={saving || !title.trim()} | |
| className="flex-1 px-4 py-3 rounded-xl bg-gradient-to-r from-primary to-indigo-600 text-white font-semibold hover:shadow-lg hover:shadow-primary/30 disabled:opacity-50 transition-all flex items-center justify-center gap-2" | |
| > | |
| {saving ? <><Loader2 className="w-4 h-4 animate-spin" /> Creating...</> : "Create Task"} | |
| </button> | |
| </div> | |
| </form> | |
| </motion.div> | |
| </div> | |
| </> | |
| )} | |
| </AnimatePresence> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 8.44 kB
- Xet hash:
- 947dfe069d3156eafdda1b8d6b9a9ecaa259619df591ef14901723cf2df7134b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.