From bc01443f03aaf32e468e1a0d7640690599d46a95 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Wed, 22 Apr 2026 20:29:33 +0000 Subject: [PATCH] feat: Notes tab (notes + shopping lists), fix 7-day forecast layout, fix screensaver dismiss --- app/api/notes/route.ts | 76 +++++++++ app/page.tsx | 33 ++-- components/NotesTab.tsx | 342 ++++++++++++++++++++++++++++++++++++++++ components/Sidebar.tsx | 5 +- 4 files changed, 445 insertions(+), 11 deletions(-) create mode 100644 app/api/notes/route.ts create mode 100644 components/NotesTab.tsx diff --git a/app/api/notes/route.ts b/app/api/notes/route.ts new file mode 100644 index 0000000..a070752 --- /dev/null +++ b/app/api/notes/route.ts @@ -0,0 +1,76 @@ +export const dynamic = 'force-dynamic' +import { NextResponse } from 'next/server' +import * as fs from 'fs' + +const NOTES_PATH = '/tmp/tablet-notes.json' + +interface Note { + id: string + type: 'note' | 'shopping' + title: string + items?: { id: string; text: string; done: boolean }[] + text?: string + color: string + createdAt: string + updatedAt: string +} + +function loadNotes(): Note[] { + try { + if (fs.existsSync(NOTES_PATH)) { + return JSON.parse(fs.readFileSync(NOTES_PATH, 'utf-8')) + } + } catch {} + return [] +} + +function saveNotes(notes: Note[]) { + fs.writeFileSync(NOTES_PATH, JSON.stringify(notes, null, 2)) +} + +export async function GET() { + return NextResponse.json({ notes: loadNotes() }) +} + +export async function POST(req: Request) { + const body = await req.json() + const notes = loadNotes() + const note: Note = { + id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), + type: body.type || 'note', + title: body.title || '', + items: body.type === 'shopping' ? [] : undefined, + text: body.type === 'note' ? '' : undefined, + color: body.color || '#6366f1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + notes.unshift(note) + saveNotes(notes) + return NextResponse.json({ note }) +} + +export async function PUT(req: Request) { + const body = await req.json() + const { id, ...updates } = body + if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 }) + + const notes = loadNotes() + const idx = notes.findIndex(n => n.id === id) + if (idx === -1) return NextResponse.json({ error: 'not found' }, { status: 404 }) + + notes[idx] = { ...notes[idx], ...updates, updatedAt: new Date().toISOString() } + saveNotes(notes) + return NextResponse.json({ note: notes[idx] }) +} + +export async function DELETE(req: Request) { + const { searchParams } = new URL(req.url) + const id = searchParams.get('id') + if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 }) + + const notes = loadNotes() + const filtered = notes.filter(n => n.id !== id) + saveNotes(filtered) + return NextResponse.json({ success: true }) +} diff --git a/app/page.tsx b/app/page.tsx index 67ffc0a..16c7397 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,9 +8,10 @@ import TopBar from '@/components/TopBar' import RoomTabs from '@/components/RoomTabs' import DeviceCard from '@/components/DeviceCard' import CalendarTab from '@/components/CalendarTab' +import NotesTab from '@/components/NotesTab' import WeatherAnimation from '@/components/WeatherAnimation' -type Tab = 'home' | 'devices' | 'calendar' | 'settings' +type Tab = 'home' | 'devices' | 'calendar' | 'notes' | 'settings' interface WeatherData { temp: string @@ -333,15 +334,23 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S {weather.forecast && weather.forecast.length > 0 && ( -
- {weather.forecast.map(day => { +
+ {weather.forecast.map((day, idx) => { const d = new Date(day.date) + const isToday = idx === 0 return ( -
-
{d.toLocaleDateString('ru-RU', { weekday: 'short' })}
-
{getWeatherIcon(day.desc)}
-
{day.maxTemp}°
-
{day.minTemp}°
+
+
+ {isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'short' })} +
+
{getWeatherIcon(day.desc)}
+
{day.maxTemp}°
+
{day.minTemp}°
) })} @@ -672,7 +681,7 @@ function HomePageInner() { // Screensaver idle detection const resetIdle = useCallback(() => { - if (screensaverActive) { setScreensaverActive(false); return } + if (screensaverActive) return // don't reset timer while screensaver is active if (idleTimer.current) clearTimeout(idleTimer.current) idleTimer.current = setTimeout(() => setScreensaverActive(true), 2 * 60 * 1000) // 2 min }, [screensaverActive]) @@ -758,6 +767,12 @@ function HomePageInner() { )} + {tab === 'notes' && ( + + + + )} + {tab === 'settings' && ( diff --git a/components/NotesTab.tsx b/components/NotesTab.tsx new file mode 100644 index 0000000..40fbe7f --- /dev/null +++ b/components/NotesTab.tsx @@ -0,0 +1,342 @@ +'use client' +import { useState, useEffect, useCallback } from 'react' +import { Plus, X, Trash2, ShoppingCart, FileText, Check, Circle } 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 + createdAt: string + updatedAt: string +} + +const NOTE_COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#3b82f6', '#8b5cf6'] + +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 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 ( + + ) + })} + + {!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, + }} + /> + +
+ + {/* 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 */ +