Files
pulse-web/src/components/CreateTaskModal.jsx

349 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { X, ChevronDown, ChevronUp, Calendar, Clock } from "lucide-react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { tasksApi } from "../api/tasks"
import clsx from "clsx"
import { format, addDays } from "date-fns"
const COLORS = [
"#6B7280", "#6366f1", "#8b5cf6", "#d946ef", "#ec4899",
"#f43f5e", "#f97316", "#eab308", "#22c55e", "#0ea5e9",
]
const ICON_CATEGORIES = [
{ name: "Продуктивность", icons: ["📋", "📝", "✅", "📌", "🎯", "💡", "📅", "⏰"] },
{ name: "Работа", icons: ["💼", "💻", "📧", "📞", "📊", "📈", "🖥️", "⌨️"] },
{ name: "Дом", icons: ["🏠", "🧹", "🧺", "🍳", "🛒", "🔧", "🪴", "🛋️"] },
{ name: "Финансы", icons: ["💰", "💳", "📊", "💵", "🏦", "🧾"] },
{ name: "Здоровье", icons: ["💊", "🏃", "🧘", "💪", "🩺", "🦷"] },
{ name: "Разное", icons: ["⭐", "🎁", "📦", "✈️", "🚗", "📷", "🎉"] },
]
const PRIORITIES = [
{ value: 0, label: "Без приоритета", color: "bg-gray-100 text-gray-600" },
{ value: 1, label: "Низкий", color: "bg-blue-100 text-blue-700" },
{ value: 2, label: "Средний", color: "bg-yellow-100 text-yellow-700" },
{ value: 3, label: "Высокий", color: "bg-red-100 text-red-700" },
]
export default function CreateTaskModal({ open, onClose, defaultDueDate = null }) {
const today = format(new Date(), "yyyy-MM-dd")
const tomorrow = format(addDays(new Date(), 1), "yyyy-MM-dd")
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [color, setColor] = useState(COLORS[0])
const [icon, setIcon] = useState("📋")
const [dueDate, setDueDate] = useState(defaultDueDate || today)
const [priority, setPriority] = useState(0)
const [reminderTime, setReminderTime] = useState("")
const [error, setError] = useState("")
const [showAllIcons, setShowAllIcons] = useState(false)
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (data) => tasksApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tasks"] })
queryClient.invalidateQueries({ queryKey: ["tasks-today"] })
handleClose()
},
onError: (err) => {
setError(err.response?.data?.error || "Ошибка создания")
},
})
const handleClose = () => {
setTitle("")
setDescription("")
setColor(COLORS[0])
setIcon("📋")
setDueDate(defaultDueDate || today)
setPriority(0)
setReminderTime("")
setError("")
setShowAllIcons(false)
onClose()
}
const handleSubmit = (e) => {
e.preventDefault()
if (!title.trim()) {
setError("Введи название задачи")
return
}
mutation.mutate({
title,
description,
color,
icon,
due_date: dueDate || null,
priority,
reminder_time: reminderTime || null,
})
}
const popularIcons = ["📋", "📝", "✅", "🎯", "💼", "🏠", "💰", "📞"]
return (
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={handleClose}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
/>
<motion.div
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 100 }}
className="fixed bottom-0 left-0 right-0 z-50 p-4 sm:p-0 sm:flex sm:items-center sm:justify-center sm:inset-0"
>
<div className="bg-white rounded-2xl sm:rounded-2xl w-full max-w-md max-h-[85vh] overflow-auto">
<div className="sticky top-0 bg-white border-b border-gray-100 px-4 py-3 flex items-center justify-between">
<h2 className="text-lg font-semibold">Новая задача</h2>
<button
onClick={handleClose}
className="p-2 text-gray-400 hover:text-gray-600 rounded-xl hover:bg-gray-100"
>
<X size={20} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{error && (
<div className="p-3 rounded-xl bg-red-50 text-red-600 text-sm">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Название
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="input"
placeholder="Что нужно сделать?"
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">
Описание (опционально)
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="input min-h-[80px] resize-none"
placeholder="Подробности задачи..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Срок выполнения
</label>
<div className="flex gap-2 mb-2">
<button
type="button"
onClick={() => setDueDate(today)}
className={clsx(
"px-3 py-1.5 rounded-lg text-sm font-medium transition-all",
dueDate === today
? "bg-primary-500 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
)}
>
Сегодня
</button>
<button
type="button"
onClick={() => setDueDate(tomorrow)}
className={clsx(
"px-3 py-1.5 rounded-lg text-sm font-medium transition-all",
dueDate === tomorrow
? "bg-primary-500 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
)}
>
Завтра
</button>
<button
type="button"
onClick={() => setDueDate("")}
className={clsx(
"px-3 py-1.5 rounded-lg text-sm font-medium transition-all",
!dueDate
? "bg-primary-500 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
)}
>
Без срока
</button>
</div>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="input pl-10"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Напоминание (опционально)
</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="time"
value={reminderTime}
onChange={(e) => setReminderTime(e.target.value)}
className="input pl-10"
/>
</div>
<p className="text-xs text-gray-500 mt-1">
Получишь напоминание в Telegram в указанное время
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Приоритет
</label>
<div className="flex gap-2 flex-wrap">
{PRIORITIES.map((p) => (
<button
key={p.value}
type="button"
onClick={() => setPriority(p.value)}
className={clsx(
"px-3 py-1.5 rounded-lg text-sm font-medium transition-all",
priority === p.value
? p.color + " ring-2 ring-offset-1 ring-gray-400"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
)}
>
{p.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Иконка
</label>
<div className="flex flex-wrap gap-2">
{popularIcons.map((ic) => (
<button
key={ic}
type="button"
onClick={() => setIcon(ic)}
className={clsx(
"w-10 h-10 rounded-xl flex items-center justify-center text-xl transition-all",
icon === ic
? "bg-primary-100 ring-2 ring-primary-500"
: "bg-gray-100 hover:bg-gray-200"
)}
>
{ic}
</button>
))}
</div>
<button
type="button"
onClick={() => setShowAllIcons(!showAllIcons)}
className="mt-2 flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700"
>
{showAllIcons ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
{showAllIcons ? "Скрыть" : "Все иконки"}
</button>
<AnimatePresence>
{showAllIcons && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="mt-3 space-y-3"
>
{ICON_CATEGORIES.map((category) => (
<div key={category.name}>
<p className="text-xs text-gray-500 mb-1.5">{category.name}</p>
<div className="flex flex-wrap gap-1.5">
{category.icons.map((ic) => (
<button
key={ic}
type="button"
onClick={() => setIcon(ic)}
className={clsx(
"w-9 h-9 rounded-lg flex items-center justify-center text-lg transition-all",
icon === ic
? "bg-primary-100 ring-2 ring-primary-500"
: "bg-gray-100 hover:bg-gray-200"
)}
>
{ic}
</button>
))}
</div>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Цвет
</label>
<div className="flex flex-wrap gap-2">
{COLORS.map((c) => (
<button
key={c}
type="button"
onClick={() => setColor(c)}
className={clsx(
"w-8 h-8 rounded-full transition-all",
color === c ? "ring-2 ring-offset-2 ring-gray-400 scale-110" : ""
)}
style={{ backgroundColor: c }}
/>
))}
</div>
</div>
<div className="pt-2">
<button
type="submit"
disabled={mutation.isPending}
className="btn btn-primary w-full"
>
{mutation.isPending ? "Создаём..." : "Создать задачу"}
</button>
</div>
</form>
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}