From 8d32e7ebb0517d819f5d0bfb75da71f89cd57efb Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 23 Apr 2026 09:17:22 +0000 Subject: [PATCH] feat: forecast swipe nav, note swipe-to-delete, night-shift tint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WeatherDayModal now accepts the full forecast array and an onChange callback; supports horizontal drag (framer-motion) plus prev/next chevrons and a dot-indicator. Drag > 60px switches day; style uses semantic tokens (shadow-xl, surface-1). - NotesTab list items wrap each note in a motion.button with drag=x, constrained to -80px. Below it a gradient+trash reveal layer. Drag past 60px opens the existing confirmDelete modal. - HomePageInner adds a night-shift overlay (fixed, mixBlendMode multiply, rgba(255,120,40,0.12)) active 22:00-06:00, auto-checked each minute, fades in/out over 800ms. No user toggle yet — fully automatic. --- app/page.tsx | 143 +++++++++++++++++++++++++++++++++------- components/NotesTab.tsx | 41 ++++++++++-- 2 files changed, 155 insertions(+), 29 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 5718544..96ca9bc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -287,31 +287,82 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) { // ————— Home Tab ————— // ————— Weather Day Detail Modal ————— -function WeatherDayModal({ day, current, onClose }: { - day: { date: string; maxTemp: string; minTemp: string; desc: string; feelsLikeMax?: string; feelsLikeMin?: string; precipProb?: string; windSpeed?: string; humidity?: string } +type ForecastDay = { date: string; maxTemp: string; minTemp: string; desc: string; feelsLikeMax?: string; feelsLikeMin?: string; precipProb?: string; windSpeed?: string; humidity?: string } + +function WeatherDayModal({ day, days, current, onClose, onChange }: { + day: ForecastDay + days: ForecastDay[] current: WeatherData | null onClose: () => void + onChange: (d: ForecastDay) => void }) { const d = new Date(day.date) const isToday = d.toDateString() === new Date().toDateString() const dayLabel = isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' }) + const idx = days.findIndex(x => x.date === day.date) + const canPrev = idx > 0 + const canNext = idx >= 0 && idx < days.length - 1 + const go = (delta: number) => { + const next = days[idx + delta] + if (next) onChange(next) + } return (
-
e.stopPropagation()}> + { + if (info.offset.x < -60 && canNext) go(1) + else if (info.offset.x > 60 && canPrev) go(-1) + }} + initial={{ opacity: 0, scale: 0.96 }} + animate={{ opacity: 1, scale: 1 }} + transition={{ type: 'spring', stiffness: 320, damping: 28 }} + style={{ + background: 'var(--surface-1)', + border: '1px solid var(--border-subtle)', borderRadius: 28, + width: 420, maxWidth: '90vw', overflow: 'hidden', + boxShadow: 'var(--shadow-xl)', + touchAction: 'pan-y', + }} + onClick={e => e.stopPropagation()} + > {/* Hero */}
+ {/* Prev/Next chevrons */} + {canPrev && ( + + )} + {canNext && ( + + )}
@@ -327,38 +378,55 @@ function WeatherDayModal({ day, current, onClose }: {
{/* Details grid */} -
+
{[ { icon: , bg: 'rgba(251,146,60,0.1)', label: 'Ощущается', value: `${day.feelsLikeMax || '—'}° / ${day.feelsLikeMin || '—'}°` }, { icon: , bg: 'rgba(59,130,246,0.1)', label: 'Влажность', value: `${day.humidity || (isToday && current ? current.humidity : '—')}%` }, { icon: , bg: 'rgba(34,211,238,0.1)', label: 'Ветер', value: `${day.windSpeed || (isToday && current ? current.windSpeed : '—')} м/с` }, - { icon: 🌧️, bg: 'rgba(99,102,241,0.1)', label: 'Вероятность осадков', value: `${day.precipProb || '0'}%` }, + { icon: 🌧️, bg: 'rgba(99,102,241,0.1)', label: 'Осадки', value: `${day.precipProb || '0'}%` }, ].map(item => (
{item.icon}
-
{item.value}
+
{item.value}
{item.label}
))}
+ {/* Dot indicator */} + {days.length > 1 && ( +
+ {days.map((di, i) => ( +
+ )} +
-
+
) } @@ -697,7 +765,13 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S {/* Weather day detail modal */} {selectedDay && ( - setSelectedDay(null)} /> + setSelectedDay(null)} + onChange={setSelectedDay} + /> )}
) @@ -909,6 +983,7 @@ function HomePageInner() { if (typeof window !== 'undefined') return localStorage.getItem('tablet-theme') || 'dark' return 'dark' }) + const [nightShift, setNightShift] = useState(false) const idleTimer = useRef | null>(null) // Theme @@ -917,6 +992,17 @@ function HomePageInner() { localStorage.setItem('tablet-theme', theme) }, [theme]) + // Night-shift tint (22:00–06:00) + useEffect(() => { + const check = () => { + const h = new Date().getHours() + setNightShift(h >= 22 || h < 6) + } + check() + const t = setInterval(check, 60_000) + return () => clearInterval(t) + }, []) + // Auth check useEffect(() => { fetch('/api/auth') @@ -1023,6 +1109,19 @@ function HomePageInner() { + {/* Night-shift warm tint overlay */} +
+
diff --git a/components/NotesTab.tsx b/components/NotesTab.tsx index 935c3c7..65e75b9 100644 --- a/components/NotesTab.tsx +++ b/components/NotesTab.tsx @@ -1,5 +1,6 @@ '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 { @@ -144,12 +145,37 @@ export default function NotesTab() { const doneCount = note.items?.filter(i => i.done).length || 0 const totalCount = note.items?.length || 0 return ( - + + ) })}