feat: add Settings page with Telegram integration, reminder time fields
This commit is contained in:
@@ -10,6 +10,7 @@ import VerifyEmail from "./pages/VerifyEmail"
|
|||||||
import ResetPassword from "./pages/ResetPassword"
|
import ResetPassword from "./pages/ResetPassword"
|
||||||
import ForgotPassword from "./pages/ForgotPassword"
|
import ForgotPassword from "./pages/ForgotPassword"
|
||||||
import Stats from "./pages/Stats"
|
import Stats from "./pages/Stats"
|
||||||
|
import Settings from "./pages/Settings"
|
||||||
|
|
||||||
function ProtectedRoute({ children }) {
|
function ProtectedRoute({ children }) {
|
||||||
const { isAuthenticated, isLoading } = useAuthStore()
|
const { isAuthenticated, isLoading } = useAuthStore()
|
||||||
@@ -114,6 +115,14 @@ export default function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Settings />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
|||||||
14
src/api/profile.js
Normal file
14
src/api/profile.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import api from "./client"
|
||||||
|
|
||||||
|
export const profileApi = {
|
||||||
|
get: async () => {
|
||||||
|
const { data } = await api.get("/profile")
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
update: async (profileData) => {
|
||||||
|
const { data } = await api.put("/profile", profileData)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default profileApi
|
||||||
@@ -1,45 +1,46 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from "react"
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
import { X, ChevronDown, ChevronUp } from 'lucide-react'
|
import { X, ChevronDown, ChevronUp, Clock } from "lucide-react"
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { habitsApi } from '../api/habits'
|
import { habitsApi } from "../api/habits"
|
||||||
import clsx from 'clsx'
|
import clsx from "clsx"
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'#6366f1', '#8b5cf6', '#d946ef', '#ec4899', '#f43f5e',
|
"#6366f1", "#8b5cf6", "#d946ef", "#ec4899", "#f43f5e",
|
||||||
'#f97316', '#eab308', '#22c55e', '#14b8a6', '#0ea5e9',
|
"#f97316", "#eab308", "#22c55e", "#14b8a6", "#0ea5e9",
|
||||||
]
|
]
|
||||||
|
|
||||||
const ICON_CATEGORIES = [
|
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: ["👥", "💬", "📞", "👨👩👧👦", "❤️"] },
|
||||||
{ name: 'Хобби', icons: ['🎨', '🎵', '🎸', '🎮', '📷', '✈️', '🚗'] },
|
{ name: "Хобби", icons: ["🎨", "🎵", "🎸", "🎮", "📷", "✈️", "🚗"] },
|
||||||
{ name: 'Еда/вода', icons: ['🥗', '🍎', '🥤', '☕', '🍽️', '💧'] },
|
{ name: "Еда/вода", icons: ["🥗", "🍎", "🥤", "☕", "🍽️", "💧"] },
|
||||||
{ name: 'Разное', icons: ['⭐', '🎉', '✨', '🔥', '🌟', '💎', '🎁'] },
|
{ name: "Разное", icons: ["⭐", "🎉", "✨", "🔥", "🌟", "💎", "🎁"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const DAYS = [
|
const DAYS = [
|
||||||
{ id: 1, short: 'Пн', full: 'Понедельник' },
|
{ id: 1, short: "Пн", full: "Понедельник" },
|
||||||
{ id: 2, short: 'Вт', full: 'Вторник' },
|
{ id: 2, short: "Вт", full: "Вторник" },
|
||||||
{ id: 3, short: 'Ср', full: 'Среда' },
|
{ id: 3, short: "Ср", full: "Среда" },
|
||||||
{ id: 4, short: 'Чт', full: 'Четверг' },
|
{ id: 4, short: "Чт", full: "Четверг" },
|
||||||
{ id: 5, short: 'Пт', full: 'Пятница' },
|
{ id: 5, short: "Пт", full: "Пятница" },
|
||||||
{ id: 6, short: 'Сб', full: 'Суббота' },
|
{ id: 6, short: "Сб", full: "Суббота" },
|
||||||
{ id: 7, short: 'Вс', full: 'Воскресенье' },
|
{ id: 7, short: "Вс", full: "Воскресенье" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function CreateHabitModal({ open, onClose }) {
|
export default function CreateHabitModal({ open, onClose }) {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState("")
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState("")
|
||||||
const [color, setColor] = useState(COLORS[0])
|
const [color, setColor] = useState(COLORS[0])
|
||||||
const [icon, setIcon] = useState('✨')
|
const [icon, setIcon] = useState("✨")
|
||||||
const [frequency, setFrequency] = useState('daily')
|
const [frequency, setFrequency] = useState("daily")
|
||||||
const [targetDays, setTargetDays] = useState([1, 2, 3, 4, 5, 6, 7])
|
const [targetDays, setTargetDays] = useState([1, 2, 3, 4, 5, 6, 7])
|
||||||
const [error, setError] = useState('')
|
const [reminderTime, setReminderTime] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
const [showAllIcons, setShowAllIcons] = useState(false)
|
const [showAllIcons, setShowAllIcons] = useState(false)
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -47,23 +48,24 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data) => habitsApi.create(data),
|
mutationFn: (data) => habitsApi.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['habits'] })
|
queryClient.invalidateQueries({ queryKey: ["habits"] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
queryClient.invalidateQueries({ queryKey: ["stats"] })
|
||||||
handleClose()
|
handleClose()
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.response?.data?.error || 'Ошибка создания')
|
setError(err.response?.data?.error || "Ошибка создания")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setName('')
|
setName("")
|
||||||
setDescription('')
|
setDescription("")
|
||||||
setColor(COLORS[0])
|
setColor(COLORS[0])
|
||||||
setIcon('✨')
|
setIcon("✨")
|
||||||
setFrequency('daily')
|
setFrequency("daily")
|
||||||
setTargetDays([1, 2, 3, 4, 5, 6, 7])
|
setTargetDays([1, 2, 3, 4, 5, 6, 7])
|
||||||
setError('')
|
setReminderTime("")
|
||||||
|
setError("")
|
||||||
setShowAllIcons(false)
|
setShowAllIcons(false)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
@@ -71,18 +73,21 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
setError('Введи название привычки')
|
setError("Введи название привычки")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (frequency === 'weekly' && targetDays.length === 0) {
|
if (frequency === "weekly" && targetDays.length === 0) {
|
||||||
setError('Выбери хотя бы один день недели')
|
setError("Выбери хотя бы один день недели")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { name, description, color, icon, frequency }
|
const data = { name, description, color, icon, frequency }
|
||||||
if (frequency === 'weekly') {
|
if (frequency === "weekly") {
|
||||||
data.target_days = targetDays
|
data.target_days = targetDays
|
||||||
}
|
}
|
||||||
|
if (reminderTime) {
|
||||||
|
data.reminder_time = reminderTime
|
||||||
|
}
|
||||||
|
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
@@ -95,8 +100,7 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Популярные иконки для быстрого выбора
|
const popularIcons = ["✨", "💪", "📚", "🏃", "💧", "🧘", "💤", "🎯", "✏️", "🍎"]
|
||||||
const popularIcons = ['✨', '💪', '📚', '🏃', '💧', '🧘', '💤', '🎯', '✏️', '🍎']
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -167,24 +171,24 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFrequency('daily')}
|
onClick={() => setFrequency("daily")}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all',
|
"flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all",
|
||||||
frequency === 'daily'
|
frequency === "daily"
|
||||||
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/30'
|
? "bg-primary-500 text-white shadow-lg shadow-primary-500/30"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Ежедневно
|
Ежедневно
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFrequency('weekly')}
|
onClick={() => setFrequency("weekly")}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all',
|
"flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all",
|
||||||
frequency === 'weekly'
|
frequency === "weekly"
|
||||||
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/30'
|
? "bg-primary-500 text-white shadow-lg shadow-primary-500/30"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
По дням недели
|
По дням недели
|
||||||
@@ -192,10 +196,10 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{frequency === 'weekly' && (
|
{frequency === "weekly" && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -208,10 +212,10 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleDay(day.id)}
|
onClick={() => toggleDay(day.id)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 py-2 rounded-lg font-medium text-sm transition-all',
|
"flex-1 py-2 rounded-lg font-medium text-sm transition-all",
|
||||||
targetDays.includes(day.id)
|
targetDays.includes(day.id)
|
||||||
? 'bg-primary-500 text-white shadow-md'
|
? "bg-primary-500 text-white shadow-md"
|
||||||
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-500 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{day.short}
|
{day.short}
|
||||||
@@ -221,6 +225,24 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
</motion.div>
|
</motion.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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Иконка
|
Иконка
|
||||||
@@ -232,10 +254,10 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -249,14 +271,14 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
className="mt-2 flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700"
|
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 ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
{showAllIcons ? 'Скрыть' : 'Все иконки'}
|
{showAllIcons ? "Скрыть" : "Все иконки"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showAllIcons && (
|
{showAllIcons && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
className="mt-3 space-y-3"
|
className="mt-3 space-y-3"
|
||||||
>
|
>
|
||||||
@@ -270,10 +292,10 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -298,8 +320,8 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setColor(c)}
|
onClick={() => setColor(c)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-8 h-8 rounded-full transition-all',
|
"w-8 h-8 rounded-full transition-all",
|
||||||
color === c ? 'ring-2 ring-offset-2 ring-gray-400 scale-110' : ''
|
color === c ? "ring-2 ring-offset-2 ring-gray-400 scale-110" : ""
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c }}
|
style={{ backgroundColor: c }}
|
||||||
/>
|
/>
|
||||||
@@ -313,7 +335,7 @@ export default function CreateHabitModal({ open, onClose }) {
|
|||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
>
|
>
|
||||||
{mutation.isPending ? 'Создаём...' : 'Создать привычку'}
|
{mutation.isPending ? "Создаём..." : "Создать привычку"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from "react"
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
import { X, ChevronDown, ChevronUp, Calendar } from 'lucide-react'
|
import { X, ChevronDown, ChevronUp, Calendar, Clock } from "lucide-react"
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { tasksApi } from '../api/tasks'
|
import { tasksApi } from "../api/tasks"
|
||||||
import clsx from 'clsx'
|
import clsx from "clsx"
|
||||||
import { format, addDays } from 'date-fns'
|
import { format, addDays } from "date-fns"
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'#6B7280', '#6366f1', '#8b5cf6', '#d946ef', '#ec4899',
|
"#6B7280", "#6366f1", "#8b5cf6", "#d946ef", "#ec4899",
|
||||||
'#f43f5e', '#f97316', '#eab308', '#22c55e', '#0ea5e9',
|
"#f43f5e", "#f97316", "#eab308", "#22c55e", "#0ea5e9",
|
||||||
]
|
]
|
||||||
|
|
||||||
const ICON_CATEGORIES = [
|
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 = [
|
const PRIORITIES = [
|
||||||
{ value: 0, label: 'Без приоритета', color: 'bg-gray-100 text-gray-600' },
|
{ value: 0, label: "Без приоритета", color: "bg-gray-100 text-gray-600" },
|
||||||
{ value: 1, label: 'Низкий', color: 'bg-blue-100 text-blue-700' },
|
{ value: 1, label: "Низкий", color: "bg-blue-100 text-blue-700" },
|
||||||
{ value: 2, label: 'Средний', color: 'bg-yellow-100 text-yellow-700' },
|
{ value: 2, label: "Средний", color: "bg-yellow-100 text-yellow-700" },
|
||||||
{ value: 3, label: 'Высокий', color: 'bg-red-100 text-red-700' },
|
{ value: 3, label: "Высокий", color: "bg-red-100 text-red-700" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function CreateTaskModal({ open, onClose, defaultDueDate = null }) {
|
export default function CreateTaskModal({ open, onClose, defaultDueDate = null }) {
|
||||||
const today = format(new Date(), 'yyyy-MM-dd')
|
const today = format(new Date(), "yyyy-MM-dd")
|
||||||
const tomorrow = format(addDays(new Date(), 1), 'yyyy-MM-dd')
|
const tomorrow = format(addDays(new Date(), 1), "yyyy-MM-dd")
|
||||||
|
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState("")
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState("")
|
||||||
const [color, setColor] = useState(COLORS[0])
|
const [color, setColor] = useState(COLORS[0])
|
||||||
const [icon, setIcon] = useState('📋')
|
const [icon, setIcon] = useState("📋")
|
||||||
const [dueDate, setDueDate] = useState(defaultDueDate || today)
|
const [dueDate, setDueDate] = useState(defaultDueDate || today)
|
||||||
const [priority, setPriority] = useState(0)
|
const [priority, setPriority] = useState(0)
|
||||||
const [error, setError] = useState('')
|
const [reminderTime, setReminderTime] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
const [showAllIcons, setShowAllIcons] = useState(false)
|
const [showAllIcons, setShowAllIcons] = useState(false)
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -45,23 +46,24 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data) => tasksApi.create(data),
|
mutationFn: (data) => tasksApi.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
queryClient.invalidateQueries({ queryKey: ["tasks"] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks-today'] })
|
queryClient.invalidateQueries({ queryKey: ["tasks-today"] })
|
||||||
handleClose()
|
handleClose()
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.response?.data?.error || 'Ошибка создания')
|
setError(err.response?.data?.error || "Ошибка создания")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setTitle('')
|
setTitle("")
|
||||||
setDescription('')
|
setDescription("")
|
||||||
setColor(COLORS[0])
|
setColor(COLORS[0])
|
||||||
setIcon('📋')
|
setIcon("📋")
|
||||||
setDueDate(defaultDueDate || today)
|
setDueDate(defaultDueDate || today)
|
||||||
setPriority(0)
|
setPriority(0)
|
||||||
setError('')
|
setReminderTime("")
|
||||||
|
setError("")
|
||||||
setShowAllIcons(false)
|
setShowAllIcons(false)
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
setError('Введи название задачи')
|
setError("Введи название задачи")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +82,11 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
icon,
|
icon,
|
||||||
due_date: dueDate || null,
|
due_date: dueDate || null,
|
||||||
priority,
|
priority,
|
||||||
|
reminder_time: reminderTime || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const popularIcons = ['📋', '📝', '✅', '🎯', '💼', '🏠', '💰', '📞']
|
const popularIcons = ["📋", "📝", "✅", "🎯", "💼", "🏠", "💰", "📞"]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -155,10 +158,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDueDate(today)}
|
onClick={() => setDueDate(today)}
|
||||||
className={clsx(
|
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
|
dueDate === today
|
||||||
? 'bg-primary-500 text-white'
|
? "bg-primary-500 text-white"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Сегодня
|
Сегодня
|
||||||
@@ -167,22 +170,22 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDueDate(tomorrow)}
|
onClick={() => setDueDate(tomorrow)}
|
||||||
className={clsx(
|
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
|
dueDate === tomorrow
|
||||||
? 'bg-primary-500 text-white'
|
? "bg-primary-500 text-white"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Завтра
|
Завтра
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDueDate('')}
|
onClick={() => setDueDate("")}
|
||||||
className={clsx(
|
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
|
!dueDate
|
||||||
? 'bg-primary-500 text-white'
|
? "bg-primary-500 text-white"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "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>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<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"
|
type="button"
|
||||||
onClick={() => setPriority(p.value)}
|
onClick={() => setPriority(p.value)}
|
||||||
className={clsx(
|
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
|
priority === p.value
|
||||||
? p.color + ' ring-2 ring-offset-1 ring-gray-400'
|
? p.color + " ring-2 ring-offset-1 ring-gray-400"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{p.label}
|
{p.label}
|
||||||
@@ -233,10 +254,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{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"
|
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 ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
{showAllIcons ? 'Скрыть' : 'Все иконки'}
|
{showAllIcons ? "Скрыть" : "Все иконки"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showAllIcons && (
|
{showAllIcons && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
className="mt-3 space-y-3"
|
className="mt-3 space-y-3"
|
||||||
>
|
>
|
||||||
@@ -271,10 +292,10 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -299,8 +320,8 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setColor(c)}
|
onClick={() => setColor(c)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-8 h-8 rounded-full transition-all',
|
"w-8 h-8 rounded-full transition-all",
|
||||||
color === c ? 'ring-2 ring-offset-2 ring-gray-400 scale-110' : ''
|
color === c ? "ring-2 ring-offset-2 ring-gray-400 scale-110" : ""
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c }}
|
style={{ backgroundColor: c }}
|
||||||
/>
|
/>
|
||||||
@@ -314,7 +335,7 @@ export default function CreateTaskModal({ open, onClose, defaultDueDate = null }
|
|||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
>
|
>
|
||||||
{mutation.isPending ? 'Создаём...' : 'Создать задачу'}
|
{mutation.isPending ? "Создаём..." : "Создать задачу"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,45 +1,46 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from "react"
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
import { X, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
|
import { X, Trash2, ChevronDown, ChevronUp, Clock } from "lucide-react"
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { habitsApi } from '../api/habits'
|
import { habitsApi } from "../api/habits"
|
||||||
import clsx from 'clsx'
|
import clsx from "clsx"
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'#6366f1', '#8b5cf6', '#d946ef', '#ec4899', '#f43f5e',
|
"#6366f1", "#8b5cf6", "#d946ef", "#ec4899", "#f43f5e",
|
||||||
'#f97316', '#eab308', '#22c55e', '#14b8a6', '#0ea5e9',
|
"#f97316", "#eab308", "#22c55e", "#14b8a6", "#0ea5e9",
|
||||||
]
|
]
|
||||||
|
|
||||||
const ICON_CATEGORIES = [
|
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: ["👥", "💬", "📞", "👨👩👧👦", "❤️"] },
|
||||||
{ name: 'Хобби', icons: ['🎨', '🎵', '🎸', '🎮', '📷', '✈️', '🚗'] },
|
{ name: "Хобби", icons: ["🎨", "🎵", "🎸", "🎮", "📷", "✈️", "🚗"] },
|
||||||
{ name: 'Еда/вода', icons: ['🥗', '🍎', '🥤', '☕', '🍽️', '💧'] },
|
{ name: "Еда/вода", icons: ["🥗", "🍎", "🥤", "☕", "🍽️", "💧"] },
|
||||||
{ name: 'Разное', icons: ['⭐', '🎉', '✨', '🔥', '🌟', '💎', '🎁'] },
|
{ name: "Разное", icons: ["⭐", "🎉", "✨", "🔥", "🌟", "💎", "🎁"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
const DAYS = [
|
const DAYS = [
|
||||||
{ id: 1, short: 'Пн' },
|
{ id: 1, short: "Пн" },
|
||||||
{ id: 2, short: 'Вт' },
|
{ id: 2, short: "Вт" },
|
||||||
{ id: 3, short: 'Ср' },
|
{ id: 3, short: "Ср" },
|
||||||
{ id: 4, short: 'Чт' },
|
{ id: 4, short: "Чт" },
|
||||||
{ id: 5, short: 'Пт' },
|
{ id: 5, short: "Пт" },
|
||||||
{ id: 6, short: 'Сб' },
|
{ id: 6, short: "Сб" },
|
||||||
{ id: 7, short: 'Вс' },
|
{ id: 7, short: "Вс" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function EditHabitModal({ open, onClose, habit }) {
|
export default function EditHabitModal({ open, onClose, habit }) {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState("")
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState("")
|
||||||
const [color, setColor] = useState(COLORS[0])
|
const [color, setColor] = useState(COLORS[0])
|
||||||
const [icon, setIcon] = useState('✨')
|
const [icon, setIcon] = useState("✨")
|
||||||
const [frequency, setFrequency] = useState('daily')
|
const [frequency, setFrequency] = useState("daily")
|
||||||
const [targetDays, setTargetDays] = useState([1, 2, 3, 4, 5, 6, 7])
|
const [targetDays, setTargetDays] = useState([1, 2, 3, 4, 5, 6, 7])
|
||||||
const [error, setError] = useState('')
|
const [reminderTime, setReminderTime] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
const [showAllIcons, setShowAllIcons] = useState(false)
|
const [showAllIcons, setShowAllIcons] = useState(false)
|
||||||
|
|
||||||
@@ -47,13 +48,14 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (habit && open) {
|
if (habit && open) {
|
||||||
setName(habit.name || '')
|
setName(habit.name || "")
|
||||||
setDescription(habit.description || '')
|
setDescription(habit.description || "")
|
||||||
setColor(habit.color || COLORS[0])
|
setColor(habit.color || COLORS[0])
|
||||||
setIcon(habit.icon || '✨')
|
setIcon(habit.icon || "✨")
|
||||||
setFrequency(habit.frequency || 'daily')
|
setFrequency(habit.frequency || "daily")
|
||||||
setTargetDays(habit.target_days || [1, 2, 3, 4, 5, 6, 7])
|
setTargetDays(habit.target_days || [1, 2, 3, 4, 5, 6, 7])
|
||||||
setError('')
|
setReminderTime(habit.reminder_time || "")
|
||||||
|
setError("")
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
setShowAllIcons(false)
|
setShowAllIcons(false)
|
||||||
}
|
}
|
||||||
@@ -62,29 +64,29 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data) => habitsApi.update(habit.id, data),
|
mutationFn: (data) => habitsApi.update(habit.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['habits'] })
|
queryClient.invalidateQueries({ queryKey: ["habits"] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
queryClient.invalidateQueries({ queryKey: ["stats"] })
|
||||||
onClose()
|
onClose()
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.response?.data?.error || 'Ошибка сохранения')
|
setError(err.response?.data?.error || "Ошибка сохранения")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: () => habitsApi.delete(habit.id),
|
mutationFn: () => habitsApi.delete(habit.id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['habits'] })
|
queryClient.invalidateQueries({ queryKey: ["habits"] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
queryClient.invalidateQueries({ queryKey: ["stats"] })
|
||||||
onClose()
|
onClose()
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.response?.data?.error || 'Ошибка удаления')
|
setError(err.response?.data?.error || "Ошибка удаления")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setError('')
|
setError("")
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
setShowAllIcons(false)
|
setShowAllIcons(false)
|
||||||
onClose()
|
onClose()
|
||||||
@@ -93,18 +95,19 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
setError('Введи название привычки')
|
setError("Введи название привычки")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (frequency === 'weekly' && targetDays.length === 0) {
|
if (frequency === "weekly" && targetDays.length === 0) {
|
||||||
setError('Выбери хотя бы один день недели')
|
setError("Выбери хотя бы один день недели")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { name, description, color, icon, frequency }
|
const data = { name, description, color, icon, frequency }
|
||||||
if (frequency === 'weekly') {
|
if (frequency === "weekly") {
|
||||||
data.target_days = targetDays
|
data.target_days = targetDays
|
||||||
}
|
}
|
||||||
|
data.reminder_time = reminderTime || null
|
||||||
|
|
||||||
updateMutation.mutate(data)
|
updateMutation.mutate(data)
|
||||||
}
|
}
|
||||||
@@ -121,7 +124,7 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const popularIcons = ['✨', '💪', '📚', '🏃', '💧', '🧘', '💤', '🎯', '✏️', '🍎']
|
const popularIcons = ["✨", "💪", "📚", "🏃", "💧", "🧘", "💤", "🎯", "✏️", "🍎"]
|
||||||
|
|
||||||
if (!habit) return null
|
if (!habit) return null
|
||||||
|
|
||||||
@@ -174,7 +177,7 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
className="flex-1 btn bg-red-500 text-white hover:bg-red-600"
|
className="flex-1 btn bg-red-500 text-white hover:bg-red-600"
|
||||||
>
|
>
|
||||||
{deleteMutation.isPending ? 'Удаляем...' : 'Удалить'}
|
{deleteMutation.isPending ? "Удаляем..." : "Удалить"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,24 +222,24 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFrequency('daily')}
|
onClick={() => setFrequency("daily")}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all',
|
"flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all",
|
||||||
frequency === 'daily'
|
frequency === "daily"
|
||||||
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/30'
|
? "bg-primary-500 text-white shadow-lg shadow-primary-500/30"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Ежедневно
|
Ежедневно
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFrequency('weekly')}
|
onClick={() => setFrequency("weekly")}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all',
|
"flex-1 py-2.5 px-4 rounded-xl font-medium text-sm transition-all",
|
||||||
frequency === 'weekly'
|
frequency === "weekly"
|
||||||
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/30'
|
? "bg-primary-500 text-white shadow-lg shadow-primary-500/30"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
По дням недели
|
По дням недели
|
||||||
@@ -244,10 +247,10 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{frequency === 'weekly' && (
|
{frequency === "weekly" && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
>
|
>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
@@ -260,10 +263,10 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleDay(day.id)}
|
onClick={() => toggleDay(day.id)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 py-2 rounded-lg font-medium text-sm transition-all',
|
"flex-1 py-2 rounded-lg font-medium text-sm transition-all",
|
||||||
targetDays.includes(day.id)
|
targetDays.includes(day.id)
|
||||||
? 'bg-primary-500 text-white shadow-md'
|
? "bg-primary-500 text-white shadow-md"
|
||||||
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-500 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{day.short}
|
{day.short}
|
||||||
@@ -273,6 +276,24 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
</motion.div>
|
</motion.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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Иконка
|
Иконка
|
||||||
@@ -284,10 +305,10 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -301,14 +322,14 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
className="mt-2 flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700"
|
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 ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
{showAllIcons ? 'Скрыть' : 'Все иконки'}
|
{showAllIcons ? "Скрыть" : "Все иконки"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showAllIcons && (
|
{showAllIcons && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
className="mt-3 space-y-3"
|
className="mt-3 space-y-3"
|
||||||
>
|
>
|
||||||
@@ -322,10 +343,10 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -350,8 +371,8 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setColor(c)}
|
onClick={() => setColor(c)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-8 h-8 rounded-full transition-all',
|
"w-8 h-8 rounded-full transition-all",
|
||||||
color === c ? 'ring-2 ring-offset-2 ring-gray-400 scale-110' : ''
|
color === c ? "ring-2 ring-offset-2 ring-gray-400 scale-110" : ""
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c }}
|
style={{ backgroundColor: c }}
|
||||||
/>
|
/>
|
||||||
@@ -365,7 +386,7 @@ export default function EditHabitModal({ open, onClose, habit }) {
|
|||||||
disabled={updateMutation.isPending}
|
disabled={updateMutation.isPending}
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
>
|
>
|
||||||
{updateMutation.isPending ? 'Сохраняем...' : 'Сохранить изменения'}
|
{updateMutation.isPending ? "Сохраняем..." : "Сохранить изменения"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from "react"
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
import { X, Trash2, ChevronDown, ChevronUp, Calendar } from 'lucide-react'
|
import { X, Trash2, ChevronDown, ChevronUp, Calendar, Clock } from "lucide-react"
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { tasksApi } from '../api/tasks'
|
import { tasksApi } from "../api/tasks"
|
||||||
import clsx from 'clsx'
|
import clsx from "clsx"
|
||||||
import { format, addDays } from 'date-fns'
|
import { format, addDays } from "date-fns"
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
'#6B7280', '#6366f1', '#8b5cf6', '#d946ef', '#ec4899',
|
"#6B7280", "#6366f1", "#8b5cf6", "#d946ef", "#ec4899",
|
||||||
'#f43f5e', '#f97316', '#eab308', '#22c55e', '#0ea5e9',
|
"#f43f5e", "#f97316", "#eab308", "#22c55e", "#0ea5e9",
|
||||||
]
|
]
|
||||||
|
|
||||||
const ICON_CATEGORIES = [
|
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 = [
|
const PRIORITIES = [
|
||||||
{ value: 0, label: 'Без приоритета', color: 'bg-gray-100 text-gray-600' },
|
{ value: 0, label: "Без приоритета", color: "bg-gray-100 text-gray-600" },
|
||||||
{ value: 1, label: 'Низкий', color: 'bg-blue-100 text-blue-700' },
|
{ value: 1, label: "Низкий", color: "bg-blue-100 text-blue-700" },
|
||||||
{ value: 2, label: 'Средний', color: 'bg-yellow-100 text-yellow-700' },
|
{ value: 2, label: "Средний", color: "bg-yellow-100 text-yellow-700" },
|
||||||
{ value: 3, label: 'Высокий', color: 'bg-red-100 text-red-700' },
|
{ value: 3, label: "Высокий", color: "bg-red-100 text-red-700" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function EditTaskModal({ open, onClose, task }) {
|
export default function EditTaskModal({ open, onClose, task }) {
|
||||||
const today = format(new Date(), 'yyyy-MM-dd')
|
const today = format(new Date(), "yyyy-MM-dd")
|
||||||
const tomorrow = format(addDays(new Date(), 1), 'yyyy-MM-dd')
|
const tomorrow = format(addDays(new Date(), 1), "yyyy-MM-dd")
|
||||||
|
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState("")
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState("")
|
||||||
const [color, setColor] = useState(COLORS[0])
|
const [color, setColor] = useState(COLORS[0])
|
||||||
const [icon, setIcon] = useState('📋')
|
const [icon, setIcon] = useState("📋")
|
||||||
const [dueDate, setDueDate] = useState('')
|
const [dueDate, setDueDate] = useState("")
|
||||||
const [priority, setPriority] = useState(0)
|
const [priority, setPriority] = useState(0)
|
||||||
const [error, setError] = useState('')
|
const [reminderTime, setReminderTime] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
const [showAllIcons, setShowAllIcons] = useState(false)
|
const [showAllIcons, setShowAllIcons] = useState(false)
|
||||||
|
|
||||||
@@ -45,13 +46,14 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (task && open) {
|
if (task && open) {
|
||||||
setTitle(task.title || '')
|
setTitle(task.title || "")
|
||||||
setDescription(task.description || '')
|
setDescription(task.description || "")
|
||||||
setColor(task.color || COLORS[0])
|
setColor(task.color || COLORS[0])
|
||||||
setIcon(task.icon || '📋')
|
setIcon(task.icon || "📋")
|
||||||
setDueDate(task.due_date || '')
|
setDueDate(task.due_date || "")
|
||||||
setPriority(task.priority || 0)
|
setPriority(task.priority || 0)
|
||||||
setError('')
|
setReminderTime(task.reminder_time || "")
|
||||||
|
setError("")
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
setShowAllIcons(false)
|
setShowAllIcons(false)
|
||||||
}
|
}
|
||||||
@@ -60,29 +62,29 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data) => tasksApi.update(task.id, data),
|
mutationFn: (data) => tasksApi.update(task.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
queryClient.invalidateQueries({ queryKey: ["tasks"] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks-today'] })
|
queryClient.invalidateQueries({ queryKey: ["tasks-today"] })
|
||||||
onClose()
|
onClose()
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.response?.data?.error || 'Ошибка сохранения')
|
setError(err.response?.data?.error || "Ошибка сохранения")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: () => tasksApi.delete(task.id),
|
mutationFn: () => tasksApi.delete(task.id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks'] })
|
queryClient.invalidateQueries({ queryKey: ["tasks"] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks-today'] })
|
queryClient.invalidateQueries({ queryKey: ["tasks-today"] })
|
||||||
onClose()
|
onClose()
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err.response?.data?.error || 'Ошибка удаления')
|
setError(err.response?.data?.error || "Ошибка удаления")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setError('')
|
setError("")
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
setShowAllIcons(false)
|
setShowAllIcons(false)
|
||||||
onClose()
|
onClose()
|
||||||
@@ -91,7 +93,7 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
setError('Введи название задачи')
|
setError("Введи название задачи")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,7 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
icon,
|
icon,
|
||||||
due_date: dueDate || null,
|
due_date: dueDate || null,
|
||||||
priority,
|
priority,
|
||||||
|
reminder_time: reminderTime || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +112,7 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
deleteMutation.mutate()
|
deleteMutation.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const popularIcons = ['📋', '📝', '✅', '🎯', '💼', '🏠', '💰', '📞']
|
const popularIcons = ["📋", "📝", "✅", "🎯", "💼", "🏠", "💰", "📞"]
|
||||||
|
|
||||||
if (!task) return null
|
if (!task) return null
|
||||||
|
|
||||||
@@ -162,7 +165,7 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
className="flex-1 btn bg-red-500 text-white hover:bg-red-600"
|
className="flex-1 btn bg-red-500 text-white hover:bg-red-600"
|
||||||
>
|
>
|
||||||
{deleteMutation.isPending ? 'Удаляем...' : 'Удалить'}
|
{deleteMutation.isPending ? "Удаляем..." : "Удалить"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,10 +211,10 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDueDate(today)}
|
onClick={() => setDueDate(today)}
|
||||||
className={clsx(
|
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
|
dueDate === today
|
||||||
? 'bg-primary-500 text-white'
|
? "bg-primary-500 text-white"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Сегодня
|
Сегодня
|
||||||
@@ -220,22 +223,22 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDueDate(tomorrow)}
|
onClick={() => setDueDate(tomorrow)}
|
||||||
className={clsx(
|
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
|
dueDate === tomorrow
|
||||||
? 'bg-primary-500 text-white'
|
? "bg-primary-500 text-white"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Завтра
|
Завтра
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDueDate('')}
|
onClick={() => setDueDate("")}
|
||||||
className={clsx(
|
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
|
!dueDate
|
||||||
? 'bg-primary-500 text-white'
|
? "bg-primary-500 text-white"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Без срока
|
Без срока
|
||||||
@@ -252,6 +255,24 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Приоритет
|
Приоритет
|
||||||
@@ -263,10 +284,10 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPriority(p.value)}
|
onClick={() => setPriority(p.value)}
|
||||||
className={clsx(
|
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
|
priority === p.value
|
||||||
? p.color + ' ring-2 ring-offset-1 ring-gray-400'
|
? p.color + " ring-2 ring-offset-1 ring-gray-400"
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{p.label}
|
{p.label}
|
||||||
@@ -286,10 +307,10 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -303,14 +324,14 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
className="mt-2 flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700"
|
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 ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
{showAllIcons ? 'Скрыть' : 'Все иконки'}
|
{showAllIcons ? "Скрыть" : "Все иконки"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showAllIcons && (
|
{showAllIcons && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
className="mt-3 space-y-3"
|
className="mt-3 space-y-3"
|
||||||
>
|
>
|
||||||
@@ -324,10 +345,10 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIcon(ic)}
|
onClick={() => setIcon(ic)}
|
||||||
className={clsx(
|
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
|
icon === ic
|
||||||
? 'bg-primary-100 ring-2 ring-primary-500'
|
? "bg-primary-100 ring-2 ring-primary-500"
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: "bg-gray-100 hover:bg-gray-200"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ic}
|
{ic}
|
||||||
@@ -352,8 +373,8 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setColor(c)}
|
onClick={() => setColor(c)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-8 h-8 rounded-full transition-all',
|
"w-8 h-8 rounded-full transition-all",
|
||||||
color === c ? 'ring-2 ring-offset-2 ring-gray-400 scale-110' : ''
|
color === c ? "ring-2 ring-offset-2 ring-gray-400 scale-110" : ""
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c }}
|
style={{ backgroundColor: c }}
|
||||||
/>
|
/>
|
||||||
@@ -367,7 +388,7 @@ export default function EditTaskModal({ open, onClose, task }) {
|
|||||||
disabled={updateMutation.isPending}
|
disabled={updateMutation.isPending}
|
||||||
className="btn btn-primary w-full"
|
className="btn btn-primary w-full"
|
||||||
>
|
>
|
||||||
{updateMutation.isPending ? 'Сохраняем...' : 'Сохранить изменения'}
|
{updateMutation.isPending ? "Сохраняем..." : "Сохранить изменения"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from "react-router-dom"
|
||||||
import { Home, ListChecks, CheckSquare, BarChart3 } from 'lucide-react'
|
import { Home, ListChecks, CheckSquare, BarChart3, Settings } from "lucide-react"
|
||||||
import clsx from 'clsx'
|
import clsx from "clsx"
|
||||||
|
|
||||||
export default function Navigation() {
|
export default function Navigation() {
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ to: '/', icon: Home, label: 'Сегодня' },
|
{ to: "/", icon: Home, label: "Сегодня" },
|
||||||
{ to: '/habits', icon: ListChecks, label: 'Привычки' },
|
{ to: "/habits", icon: ListChecks, label: "Привычки" },
|
||||||
{ to: '/tasks', icon: CheckSquare, label: 'Задачи' },
|
{ to: "/tasks", icon: CheckSquare, label: "Задачи" },
|
||||||
{ to: '/stats', icon: BarChart3, label: 'Статистика' },
|
{ to: "/stats", icon: BarChart3, label: "Статистика" },
|
||||||
|
{ to: "/settings", icon: Settings, label: "Настройки" },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,14 +21,14 @@ export default function Navigation() {
|
|||||||
to={to}
|
to={to}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
clsx(
|
clsx(
|
||||||
'flex flex-col items-center gap-1 px-3 py-2 rounded-xl transition-all',
|
"flex flex-col items-center gap-1 px-2 py-2 rounded-xl transition-all",
|
||||||
isActive
|
isActive
|
||||||
? 'text-primary-600 bg-primary-50'
|
? "text-primary-600 bg-primary-50"
|
||||||
: 'text-gray-400 hover:text-gray-600'
|
: "text-gray-400 hover:text-gray-600"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon size={22} />
|
<Icon size={20} />
|
||||||
<span className="text-xs font-medium">{label}</span>
|
<span className="text-xs font-medium">{label}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
|
|||||||
224
src/pages/Settings.jsx
Normal file
224
src/pages/Settings.jsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { ArrowLeft, Bell, MessageCircle, Globe, Save, Copy, Check } from "lucide-react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { profileApi } from "../api/profile"
|
||||||
|
import Navigation from "../components/Navigation"
|
||||||
|
|
||||||
|
const TIMEZONES = [
|
||||||
|
{ value: "Europe/Moscow", label: "Москва (UTC+3)" },
|
||||||
|
{ value: "Europe/Kaliningrad", label: "Калининград (UTC+2)" },
|
||||||
|
{ value: "Europe/Samara", label: "Самара (UTC+4)" },
|
||||||
|
{ value: "Asia/Yekaterinburg", label: "Екатеринбург (UTC+5)" },
|
||||||
|
{ value: "Asia/Omsk", label: "Омск (UTC+6)" },
|
||||||
|
{ value: "Asia/Krasnoyarsk", label: "Красноярск (UTC+7)" },
|
||||||
|
{ value: "Asia/Irkutsk", label: "Иркутск (UTC+8)" },
|
||||||
|
{ value: "Asia/Yakutsk", label: "Якутск (UTC+9)" },
|
||||||
|
{ value: "Asia/Vladivostok", label: "Владивосток (UTC+10)" },
|
||||||
|
{ value: "Asia/Magadan", label: "Магадан (UTC+11)" },
|
||||||
|
{ value: "Asia/Kamchatka", label: "Камчатка (UTC+12)" },
|
||||||
|
{ value: "Asia/Tokyo", label: "Токио (UTC+9)" },
|
||||||
|
{ value: "Europe/London", label: "Лондон (UTC+0)" },
|
||||||
|
{ value: "Europe/Berlin", label: "Берлин (UTC+1)" },
|
||||||
|
{ value: "America/New_York", label: "Нью-Йорк (UTC-5)" },
|
||||||
|
{ value: "America/Los_Angeles", label: "Лос-Анджелес (UTC-8)" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [chatId, setChatId] = useState("")
|
||||||
|
const [notificationsEnabled, setNotificationsEnabled] = useState(true)
|
||||||
|
const [timezone, setTimezone] = useState("Europe/Moscow")
|
||||||
|
const [hasChanges, setHasChanges] = useState(false)
|
||||||
|
|
||||||
|
const { data: profile, isLoading } = useQuery({
|
||||||
|
queryKey: ["profile"],
|
||||||
|
queryFn: profileApi.get,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile) {
|
||||||
|
setChatId(profile.telegram_chat_id?.toString() || "")
|
||||||
|
setNotificationsEnabled(profile.notifications_enabled ?? true)
|
||||||
|
setTimezone(profile.timezone || "Europe/Moscow")
|
||||||
|
}
|
||||||
|
}, [profile])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile) {
|
||||||
|
const changed =
|
||||||
|
(chatId !== (profile.telegram_chat_id?.toString() || "")) ||
|
||||||
|
notificationsEnabled !== (profile.notifications_enabled ?? true) ||
|
||||||
|
timezone !== (profile.timezone || "Europe/Moscow")
|
||||||
|
setHasChanges(changed)
|
||||||
|
}
|
||||||
|
}, [chatId, notificationsEnabled, timezone, profile])
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: profileApi.update,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["profile"] })
|
||||||
|
setHasChanges(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const data = {
|
||||||
|
notifications_enabled: notificationsEnabled,
|
||||||
|
timezone: timezone,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatId) {
|
||||||
|
data.telegram_chat_id = parseInt(chatId, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation.mutate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyInstruction = () => {
|
||||||
|
navigator.clipboard.writeText("@PulseNotifyBot")
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-surface-50 flex items-center justify-center">
|
||||||
|
<div className="w-10 h-10 border-4 border-primary-500 border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-surface-50 pb-24">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white/80 backdrop-blur-xl sticky top-0 z-40 border-b border-gray-100">
|
||||||
|
<div className="max-w-lg mx-auto px-4 py-4 flex items-center gap-3">
|
||||||
|
<Link to="/" className="p-2 -ml-2 text-gray-600 hover:text-gray-900 rounded-xl hover:bg-gray-100">
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-xl font-bold">Настройки</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-lg mx-auto px-4 py-6 space-y-6">
|
||||||
|
{/* Telegram Section */}
|
||||||
|
<section className="bg-white rounded-2xl p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||||
|
<MessageCircle className="text-blue-600" size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Telegram</h2>
|
||||||
|
<p className="text-sm text-gray-500">Получай уведомления в Telegram</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-3 bg-blue-50 rounded-xl">
|
||||||
|
<p className="text-sm text-blue-800 mb-2">
|
||||||
|
1. Напиши <code className="bg-blue-100 px-1 rounded">/start</code> боту в Telegram
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={copyInstruction}
|
||||||
|
className="flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
{copied ? <Check size={16} /> : <Copy size={16} />}
|
||||||
|
{copied ? "Скопировано!" : "@PulseNotifyBot"}
|
||||||
|
</button>
|
||||||
|
<p className="text-sm text-blue-800 mt-2">
|
||||||
|
2. Скопируй Chat ID из ответа бота и вставь ниже
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Chat ID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={chatId}
|
||||||
|
onChange={(e) => setChatId(e.target.value.replace(/\D/g, ""))}
|
||||||
|
placeholder="Например: 123456789"
|
||||||
|
className="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Notifications Section */}
|
||||||
|
<section className="bg-white rounded-2xl p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-orange-100 flex items-center justify-center">
|
||||||
|
<Bell className="text-orange-600" size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Уведомления</h2>
|
||||||
|
<p className="text-sm text-gray-500">Настрой push-уведомления</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center justify-between p-3 bg-gray-50 rounded-xl cursor-pointer">
|
||||||
|
<span className="text-sm font-medium">Включить уведомления</span>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={notificationsEnabled}
|
||||||
|
onChange={(e) => setNotificationsEnabled(e.target.checked)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-300 rounded-full peer peer-checked:bg-primary-500 transition-colors"></div>
|
||||||
|
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full peer-checked:translate-x-5 transition-transform"></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Timezone Section */}
|
||||||
|
<section className="bg-white rounded-2xl p-4 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-purple-100 flex items-center justify-center">
|
||||||
|
<Globe className="text-purple-600" size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Часовой пояс</h2>
|
||||||
|
<p className="text-sm text-gray-500">Для корректных напоминаний</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={timezone}
|
||||||
|
onChange={(e) => setTimezone(e.target.value)}
|
||||||
|
className="input"
|
||||||
|
>
|
||||||
|
{TIMEZONES.map((tz) => (
|
||||||
|
<option key={tz.value} value={tz.value}>
|
||||||
|
{tz.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
{hasChanges && (
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
className="btn btn-primary w-full flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Save size={18} />
|
||||||
|
{mutation.isPending ? "Сохраняем..." : "Сохранить изменения"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mutation.isSuccess && !hasChanges && (
|
||||||
|
<div className="p-3 rounded-xl bg-green-50 text-green-700 text-sm text-center">
|
||||||
|
✅ Настройки сохранены
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Navigation />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user