feat: Notes tab (notes + shopping lists), fix 7-day forecast layout, fix screensaver dismiss
All checks were successful
Deploy / deploy (push) Successful in 2m54s
All checks were successful
Deploy / deploy (push) Successful in 2m54s
This commit is contained in:
76
app/api/notes/route.ts
Normal file
76
app/api/notes/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
33
app/page.tsx
33
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
|
||||
</div>
|
||||
</div>
|
||||
{weather.forecast && weather.forecast.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{weather.forecast.map(day => {
|
||||
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', WebkitOverflowScrolling: 'touch' as any, scrollbarWidth: 'none' as any, msOverflowStyle: 'none' as any, paddingBottom: 2 }}>
|
||||
{weather.forecast.map((day, idx) => {
|
||||
const d = new Date(day.date)
|
||||
const isToday = idx === 0
|
||||
return (
|
||||
<div key={day.date} style={{ flex: 1, background: 'rgba(255,255,255,0.04)', borderRadius: 14, padding: '10px 8px', textAlign: 'center', border: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 4, fontWeight: 500 }}>{d.toLocaleDateString('ru-RU', { weekday: 'short' })}</div>
|
||||
<div style={{ fontSize: 20, marginBottom: 4 }}>{getWeatherIcon(day.desc)}</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)' }}>{day.minTemp}°</div>
|
||||
<div key={day.date} style={{
|
||||
minWidth: 58, background: isToday ? 'rgba(99,102,241,0.1)' : 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 12, padding: '8px 6px', textAlign: 'center',
|
||||
border: isToday ? '1px solid rgba(129,140,248,0.2)' : '1px solid rgba(255,255,255,0.04)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 3, fontWeight: 600 }}>
|
||||
{isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'short' })}
|
||||
</div>
|
||||
<div style={{ fontSize: 16, marginBottom: 3 }}>{getWeatherIcon(day.desc)}</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-secondary)' }}>{day.minTemp}°</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -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() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{tab === 'notes' && (
|
||||
<motion.div key="notes" variants={tabVariants} initial="enter" animate="center" exit="exit" transition={{ duration: 0.2 }} style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<NotesTab />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{tab === 'settings' && (
|
||||
<motion.div key="settings" variants={tabVariants} initial="enter" animate="center" exit="exit" transition={{ duration: 0.2 }} style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<SettingsTab city={city} onCityChange={handleCityChange} onLogout={handleLogout} theme={theme} onThemeChange={setTheme} />
|
||||
|
||||
342
components/NotesTab.tsx
Normal file
342
components/NotesTab.tsx
Normal file
@@ -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<Note[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeNote, setActiveNote] = useState<Note | null>(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<Note>) => {
|
||||
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 (
|
||||
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', padding: '16px 24px 24px', gap: 16 }}>
|
||||
{/* Left: notes list */}
|
||||
<div style={{ width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'auto' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>Заметки</h2>
|
||||
<button onClick={() => setShowCreate(v => !v)} style={{
|
||||
width: 36, height: 36, borderRadius: 12,
|
||||
background: showCreate ? 'rgba(255,255,255,0.06)' : 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))',
|
||||
border: showCreate ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(129,140,248,0.25)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: showCreate ? 'var(--text-secondary)' : '#a5b4fc',
|
||||
}}>
|
||||
{showCreate ? <X size={16} /> : <Plus size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Create new */}
|
||||
{showCreate && (
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 4 }}>
|
||||
<button onClick={() => createNote('note')} style={{
|
||||
flex: 1, padding: '14px 12px', borderRadius: 14,
|
||||
background: 'rgba(99,102,241,0.08)', border: '1px solid rgba(129,140,248,0.15)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
|
||||
color: '#a5b4fc', fontSize: 12, fontWeight: 600,
|
||||
}}>
|
||||
<FileText size={20} />
|
||||
Заметка
|
||||
</button>
|
||||
<button onClick={() => createNote('shopping')} style={{
|
||||
flex: 1, padding: '14px 12px', borderRadius: 14,
|
||||
background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.15)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
|
||||
color: '#34d399', fontSize: 12, fontWeight: 600,
|
||||
}}>
|
||||
<ShoppingCart size={20} />
|
||||
Список
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 (
|
||||
<button key={note.id} onClick={() => setActiveNote(note)} style={{
|
||||
padding: '14px 16px', borderRadius: 16, textAlign: 'left', width: '100%',
|
||||
background: isActive ? `${note.color}15` : 'rgba(255,255,255,0.025)',
|
||||
border: `1px solid ${isActive ? note.color + '30' : 'rgba(255,255,255,0.05)'}`,
|
||||
transition: 'all 0.2s ease',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
{note.type === 'shopping'
|
||||
? <ShoppingCart size={14} color={note.color} />
|
||||
: <FileText size={14} color={note.color} />
|
||||
}
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
|
||||
{note.title}
|
||||
</span>
|
||||
</div>
|
||||
{note.type === 'shopping' && totalCount > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{
|
||||
flex: 1, height: 3, borderRadius: 2, background: 'rgba(255,255,255,0.08)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${totalCount > 0 ? (doneCount / totalCount) * 100 : 0}%`,
|
||||
height: '100%', borderRadius: 2, background: note.color,
|
||||
transition: 'width 0.3s ease',
|
||||
}} />
|
||||
</div>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)', flexShrink: 0 }}>{doneCount}/{totalCount}</span>
|
||||
</div>
|
||||
)}
|
||||
{note.type === 'note' && note.text && (
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{note.text}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
{!loading && notes.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: 'var(--text-secondary)' }}>
|
||||
<FileText size={32} style={{ margin: '0 auto 12px', opacity: 0.3 }} />
|
||||
<div style={{ fontSize: 14 }}>Нет заметок</div>
|
||||
<div style={{ fontSize: 12, marginTop: 4 }}>Нажмите + чтобы создать</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: note editor */}
|
||||
<div style={{
|
||||
flex: 1, display: 'flex', flexDirection: 'column',
|
||||
background: 'rgba(255,255,255,0.02)', borderRadius: 22,
|
||||
border: '1px solid rgba(255,255,255,0.04)', overflow: 'hidden',
|
||||
}}>
|
||||
{!activeNote ? (
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-secondary)' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<FileText size={40} style={{ margin: '0 auto 12px', opacity: 0.2 }} />
|
||||
<div style={{ fontSize: 15 }}>Выберите заметку</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '18px 24px', borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
background: `${activeNote.color}08`,
|
||||
}}>
|
||||
<input
|
||||
value={activeNote.title}
|
||||
onChange={e => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => { if (confirm('Удалить заметку?')) deleteNote(activeNote.id) }} style={{
|
||||
width: 32, height: 32, borderRadius: 10,
|
||||
background: 'rgba(239,68,68,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#f87171',
|
||||
}}>
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 24px 24px' }}>
|
||||
{activeNote.type === 'shopping' ? (
|
||||
<>
|
||||
{/* Add item */}
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<input
|
||||
value={newItemText}
|
||||
onChange={e => 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,
|
||||
}}
|
||||
/>
|
||||
<button onClick={addItem} style={{
|
||||
width: 44, borderRadius: 12,
|
||||
background: `${activeNote.color}20`, border: `1px solid ${activeNote.color}30`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: activeNote.color,
|
||||
}}>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{(activeNote.items || []).filter(i => !i.done).map(item => (
|
||||
<div key={item.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: '12px 14px', borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<button onClick={() => toggleItem(item.id)} style={{
|
||||
width: 22, height: 22, borderRadius: 6,
|
||||
border: `2px solid ${activeNote.color}50`,
|
||||
background: 'transparent',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<span style={{ flex: 1, fontSize: 15, color: 'var(--text-primary)' }}>{item.text}</span>
|
||||
<button onClick={() => removeItem(item.id)} style={{ color: 'var(--text-tertiary)', padding: 4, flexShrink: 0 }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Done items */}
|
||||
{(activeNote.items || []).filter(i => i.done).length > 0 && (
|
||||
<>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: 12, marginBottom: 4 }}>
|
||||
Выполнено
|
||||
</div>
|
||||
{(activeNote.items || []).filter(i => i.done).map(item => (
|
||||
<div key={item.id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
padding: '10px 14px', borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.01)',
|
||||
opacity: 0.5,
|
||||
}}>
|
||||
<button onClick={() => toggleItem(item.id)} style={{
|
||||
width: 22, height: 22, borderRadius: 6,
|
||||
border: 'none', background: activeNote.color,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<Check size={14} color="#fff" />
|
||||
</button>
|
||||
<span style={{ flex: 1, fontSize: 15, color: 'var(--text-secondary)', textDecoration: 'line-through' }}>{item.text}</span>
|
||||
<button onClick={() => removeItem(item.id)} style={{ color: 'var(--text-tertiary)', padding: 4, flexShrink: 0 }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Text note */
|
||||
<textarea
|
||||
value={activeNote.text || ''}
|
||||
onChange={e => {
|
||||
const newText = e.target.value
|
||||
setActiveNote(prev => prev ? { ...prev, text: newText } : null)
|
||||
}}
|
||||
onBlur={() => updateNote(activeNote.id, { text: activeNote.text })}
|
||||
placeholder="Начните писать..."
|
||||
style={{
|
||||
width: '100%', height: '100%', resize: 'none',
|
||||
background: 'transparent', border: 'none', outline: 'none',
|
||||
color: 'var(--text-primary)', fontSize: 15, lineHeight: 1.7,
|
||||
fontFamily: 'inherit', boxSizing: 'border-box' as any,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { Home, Cpu, CalendarDays, Settings } from 'lucide-react'
|
||||
import { Home, Cpu, CalendarDays, StickyNote, Settings } from 'lucide-react'
|
||||
|
||||
type Tab = 'home' | 'devices' | 'calendar' | 'settings'
|
||||
type Tab = 'home' | 'devices' | 'calendar' | 'notes' | 'settings'
|
||||
|
||||
interface SidebarProps {
|
||||
active: Tab
|
||||
@@ -13,6 +13,7 @@ const navItems: { id: Tab; icon: any; label: string }[] = [
|
||||
{ id: 'home', icon: Home, label: 'Главная' },
|
||||
{ id: 'devices', icon: Cpu, label: 'Устройства' },
|
||||
{ id: 'calendar', icon: CalendarDays, label: 'Календарь' },
|
||||
{ id: 'notes', icon: StickyNote, label: 'Заметки' },
|
||||
{ id: 'settings', icon: Settings, label: 'Настройки' },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user