'use client' import { useState, useEffect, useCallback } from 'react' import { motion } from 'framer-motion' import { Plus, X, Trash2, ShoppingCart, FileText, Check, Circle, Calendar as CalendarIcon } from 'lucide-react' interface NoteItem { id: string text: string done: boolean } interface Note { id: string type: 'note' | 'shopping' title: string items?: NoteItem[] text?: string color: string pinDate: string | null createdAt: string updatedAt: string } // NOTE: заметки сохраняют свой цвет на всё время жизни — // храним hex, но генерим их из theme-aware CSS-переменных через computed style. // Для совместимости с существующими заметками оставляем hex-палитру, // но подобранную под новые data-токены (dark theme). const NOTE_COLORS = ['#818cf8', '#f472b6', '#34d399', '#fbbf24', '#38bdf8', '#a78bfa'] export default function NotesTab() { const [notes, setNotes] = useState([]) const [loading, setLoading] = useState(true) const [activeNote, setActiveNote] = useState(null) const [showCreate, setShowCreate] = useState(false) const [newItemText, setNewItemText] = useState('') const [confirmDelete, setConfirmDelete] = useState(null) const load = useCallback(async () => { try { const r = await fetch('/api/notes') const d = await r.json() setNotes(d.notes || []) } catch {} finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) const createNote = async (type: 'note' | 'shopping') => { const title = type === 'shopping' ? 'Список покупок' : 'Новая заметка' const color = NOTE_COLORS[notes.length % NOTE_COLORS.length] try { const r = await fetch('/api/notes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, title, color }), }) const d = await r.json() setNotes(prev => [d.note, ...prev]) setActiveNote(d.note) setShowCreate(false) } catch {} } const updateNote = async (id: string, updates: Partial) => { try { const r = await fetch('/api/notes', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, ...updates }), }) const d = await r.json() setNotes(prev => prev.map(n => n.id === id ? d.note : n)) if (activeNote?.id === id) setActiveNote(d.note) } catch {} } const deleteNote = async (id: string) => { try { await fetch(`/api/notes?id=${id}`, { method: 'DELETE' }) setNotes(prev => prev.filter(n => n.id !== id)) if (activeNote?.id === id) setActiveNote(null) } catch {} } const addItem = async () => { if (!newItemText.trim() || !activeNote) return const items = [...(activeNote.items || []), { id: Date.now().toString(36), text: newItemText.trim(), done: false }] await updateNote(activeNote.id, { items }) setNewItemText('') } const toggleItem = async (itemId: string) => { if (!activeNote) return const items = (activeNote.items || []).map(i => i.id === itemId ? { ...i, done: !i.done } : i) await updateNote(activeNote.id, { items }) } const removeItem = async (itemId: string) => { if (!activeNote) return const items = (activeNote.items || []).filter(i => i.id !== itemId) await updateNote(activeNote.id, { items }) } return (
{/* Left: notes list */}

Заметки

{/* Create new */} {showCreate && (
)} {/* Notes list */} {notes.map(note => { const isActive = activeNote?.id === note.id const doneCount = note.items?.filter(i => i.done).length || 0 const totalCount = note.items?.length || 0 return ( {/* Delete reveal layer */}
{ if (info.offset.x < -60) setConfirmDelete(note) }} onClick={() => setActiveNote(note)} style={{ padding: '14px 16px', borderRadius: 16, textAlign: 'left', width: '100%', background: isActive ? `${note.color}15` : 'var(--surface-2)', border: `1px solid ${isActive ? note.color + '30' : 'var(--border-subtle)'}`, transition: 'background 0.2s ease, border-color 0.2s ease', position: 'relative', zIndex: 1, touchAction: 'pan-y', }}>
{note.type === 'shopping' ? : } {note.title}
{note.pinDate && (
{new Date(note.pinDate).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })}
)} {note.type === 'shopping' && totalCount > 0 && (
0 ? (doneCount / totalCount) * 100 : 0}%`, height: '100%', borderRadius: 2, background: note.color, transition: 'width 0.3s ease', }} />
{doneCount}/{totalCount}
)} {note.type === 'note' && note.text && (
{note.text}
)} ) })} {!loading && notes.length === 0 && (
Нет заметок
Нажмите + чтобы создать
)}
{/* Right: note editor */}
{!activeNote ? (
Выберите заметку
) : ( <> {/* Header */}
{ const newTitle = e.target.value setActiveNote(prev => prev ? { ...prev, title: newTitle } : null) }} onBlur={() => updateNote(activeNote.id, { title: activeNote.title })} style={{ background: 'transparent', border: 'none', outline: 'none', fontSize: 18, fontWeight: 700, color: 'var(--text-primary)', fontFamily: 'inherit', flex: 1, minWidth: 0, }} />
{ const v = e.target.value || null setActiveNote(prev => prev ? { ...prev, pinDate: v } : null) updateNote(activeNote.id, { pinDate: v }) }} style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 10, padding: '6px 10px', color: 'var(--text-primary)', fontSize: 13, outline: 'none', fontFamily: 'inherit', colorScheme: 'dark' as any, }} /> {activeNote.pinDate && ( )}
{/* Content */}
{activeNote.type === 'shopping' ? ( <> {/* Add item */}
setNewItemText(e.target.value)} onKeyDown={e => e.key === 'Enter' && addItem()} placeholder="Добавить..." style={{ flex: 1, padding: '12px 16px', borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', color: 'var(--text-primary)', fontSize: 15, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' as any, }} />
{/* Items */}
{(activeNote.items || []).filter(i => !i.done).map(item => (
))} {/* Done items */} {(activeNote.items || []).filter(i => i.done).length > 0 && ( <>
Выполнено
{(activeNote.items || []).filter(i => i.done).map(item => (
{item.text}
))} )}
) : ( /* Text note */