tahamajs's picture
download
raw
8.44 kB
// 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.