feat: add Settings page with Telegram integration, reminder time fields
This commit is contained in:
@@ -1,43 +1,44 @@
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { X, ChevronDown, ChevronUp, Calendar } 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'
|
||||
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',
|
||||
"#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: ['⭐', '🎁', '📦', '✈️', '🚗', '📷', '🎉'] },
|
||||
{ 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' },
|
||||
{ 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 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 [title, setTitle] = useState("")
|
||||
const [description, setDescription] = useState("")
|
||||
const [color, setColor] = useState(COLORS[0])
|
||||
const [icon, setIcon] = useState('📋')
|
||||
const [icon, setIcon] = useState("📋")
|
||||
const [dueDate, setDueDate] = useState(defaultDueDate || today)
|
||||
const [priority, setPriority] = useState(0)
|
||||
const [error, setError] = useState('')
|
||||
const [reminderTime, setReminderTime] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [showAllIcons, setShowAllIcons] = useState(false)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
@@ -45,23 +46,24 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data) => tasksApi.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks-today'] })
|
||||
queryClient.invalidateQueries({ queryKey: ["tasks"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["tasks-today"] })
|
||||
handleClose()
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err.response?.data?.error || 'Ошибка создания')
|
||||
setError(err.response?.data?.error || "Ошибка создания")
|
||||
},
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
setTitle("")
|
||||
setDescription("")
|
||||
setColor(COLORS[0])
|
||||
setIcon('📋')
|
||||
setIcon("📋")
|
||||
setDueDate(defaultDueDate || today)
|
||||
setPriority(0)
|
||||
setError('')
|
||||
setReminderTime("")
|
||||
setError("")
|
||||
setShowAllIcons(false)
|
||||
onClose()
|
||||
}
|
||||
@@ -69,7 +71,7 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
if (!title.trim()) {
|
||||
setError('Введи название задачи')
|
||||
setError("Введи название задачи")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,10 +82,11 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
icon,
|
||||
due_date: dueDate || null,
|
||||
priority,
|
||||
reminder_time: reminderTime || null,
|
||||
})
|
||||
}
|
||||
|
||||
const popularIcons = ['📋', '📝', '✅', '🎯', '💼', '🏠', '💰', '📞']
|
||||
const popularIcons = ["📋", "📝", "✅", "🎯", "💼", "🏠", "💰", "📞"]
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -155,10 +158,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
type="button"
|
||||
onClick={() => setDueDate(today)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-all',
|
||||
"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'
|
||||
? "bg-primary-500 text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Сегодня
|
||||
@@ -167,22 +170,22 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
type="button"
|
||||
onClick={() => setDueDate(tomorrow)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-all',
|
||||
"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'
|
||||
? "bg-primary-500 text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Завтра
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDueDate('')}
|
||||
onClick={() => setDueDate("")}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-all',
|
||||
"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'
|
||||
? "bg-primary-500 text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
Без срока
|
||||
@@ -199,6 +202,24 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
</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">
|
||||
Приоритет
|
||||
@@ -210,10 +231,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
type="button"
|
||||
onClick={() => setPriority(p.value)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium transition-all',
|
||||
"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.color + " ring-2 ring-offset-1 ring-gray-400"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
{p.label}
|
||||
@@ -233,10 +254,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
type="button"
|
||||
onClick={() => setIcon(ic)}
|
||||
className={clsx(
|
||||
'w-10 h-10 rounded-xl flex items-center justify-center text-xl transition-all',
|
||||
"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'
|
||||
? "bg-primary-100 ring-2 ring-primary-500"
|
||||
: "bg-gray-100 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
{ic}
|
||||
@@ -250,14 +271,14 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
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 ? 'Скрыть' : 'Все иконки'}
|
||||
{showAllIcons ? "Скрыть" : "Все иконки"}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showAllIcons && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="mt-3 space-y-3"
|
||||
>
|
||||
@@ -271,10 +292,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
type="button"
|
||||
onClick={() => setIcon(ic)}
|
||||
className={clsx(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center text-lg transition-all',
|
||||
"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'
|
||||
? "bg-primary-100 ring-2 ring-primary-500"
|
||||
: "bg-gray-100 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
{ic}
|
||||
@@ -299,8 +320,8 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
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' : ''
|
||||
"w-8 h-8 rounded-full transition-all",
|
||||
color === c ? "ring-2 ring-offset-2 ring-gray-400 scale-110" : ""
|
||||
)}
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
@@ -314,7 +335,7 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
||||
disabled={mutation.isPending}
|
||||
className="btn btn-primary w-full"
|
||||
>
|
||||
{mutation.isPending ? 'Создаём...' : 'Создать задачу'}
|
||||
{mutation.isPending ? "Создаём..." : "Создать задачу"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user