feat: event editing, light/dark theme, device animations, 7-day forecast
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -155,6 +155,54 @@ export async function POST(req: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
const body = await req.json()
|
||||
const { eventId, title, date, startTime, endTime, allDay, calendarId } = body
|
||||
|
||||
if (!eventId) {
|
||||
return NextResponse.json({ error: 'eventId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const auth = getAuth(false)
|
||||
if (!auth) return NextResponse.json({ error: 'not_configured' }, { status: 500 })
|
||||
|
||||
const targetCalendarId = calendarId || process.env.DANIIL_CALENDAR_ID || 'daniilklimov25@gmail.com'
|
||||
const calendarClient = google.calendar({ version: 'v3', auth: auth as any })
|
||||
|
||||
let start: any, end: any
|
||||
if (allDay) {
|
||||
start = { date }
|
||||
end = { date }
|
||||
} else {
|
||||
start = { dateTime: , timeZone: 'Europe/Moscow' }
|
||||
end = { dateTime: , timeZone: 'Europe/Moscow' }
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await calendarClient.events.patch({
|
||||
calendarId: targetCalendarId,
|
||||
eventId,
|
||||
requestBody: {
|
||||
summary: title,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
})
|
||||
const e = res.data
|
||||
return NextResponse.json({
|
||||
event: {
|
||||
id: e.id,
|
||||
title: e.summary || title,
|
||||
start: e.start?.dateTime || e.start?.date,
|
||||
end: e.end?.dateTime || e.end?.date,
|
||||
allDay: !e.start?.dateTime,
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message || 'Failed to update event' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
export async function DELETE(req: Request) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const eventId = searchParams.get('eventId')
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function GET(req: Request) {
|
||||
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
||||
daily: "weather_code,temperature_2m_max,temperature_2m_min",
|
||||
timezone: "Europe/Moscow",
|
||||
forecast_days: "3",
|
||||
forecast_days: "7",
|
||||
});
|
||||
|
||||
const res = await fetch(url, {
|
||||
|
||||
@@ -156,3 +156,66 @@ button:focus-visible {
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
/* ————— Light theme ————— */
|
||||
.light {
|
||||
--bg: #f5f5fa;
|
||||
--bg-secondary: #eeeef4;
|
||||
--sidebar-bg: rgba(255, 255, 255, 0.8);
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-bg-hover: rgba(255, 255, 255, 0.85);
|
||||
--card-border: rgba(0, 0, 0, 0.06);
|
||||
--card-border-hover: rgba(0, 0, 0, 0.1);
|
||||
--text-primary: rgba(0, 0, 0, 0.88);
|
||||
--text-secondary: rgba(0, 0, 0, 0.45);
|
||||
--text-tertiary: rgba(0, 0, 0, 0.2);
|
||||
--accent: #6366f1;
|
||||
--accent-secondary: #0891b2;
|
||||
--accent-glow: rgba(99, 102, 241, 0.12);
|
||||
--glass: rgba(255, 255, 255, 0.5);
|
||||
--glass-border: rgba(0, 0, 0, 0.06);
|
||||
--on-color: #6366f1;
|
||||
--off-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.light .bg-ambient::before {
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
|
||||
}
|
||||
.light .bg-ambient::after {
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.04) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.light ::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.light ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ————— Device animations ————— */
|
||||
@keyframes fan-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes light-pulse {
|
||||
0%, 100% { filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.3)); }
|
||||
50% { filter: drop-shadow(0 0 12px rgba(251, 191, 36, 0.6)); }
|
||||
}
|
||||
|
||||
@keyframes device-breathe {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.fan-spinning {
|
||||
animation: fan-spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.light-on-pulse {
|
||||
animation: light-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.device-active-breathe {
|
||||
animation: device-breathe 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
45
app/page.tsx
45
app/page.tsx
@@ -391,7 +391,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
}
|
||||
|
||||
// ————— Settings Tab —————
|
||||
function SettingsTab({ city, onCityChange, onLogout }: { city: string; onCityChange: (id: string) => void; onLogout: () => void }) {
|
||||
function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { city: string; onCityChange: (id: string) => void; onLogout: () => void; theme: string; onThemeChange: (t: string) => void }) {
|
||||
const [showPinChange, setShowPinChange] = useState(false)
|
||||
const [oldPin, setOldPin] = useState('')
|
||||
const [newPin, setNewPin] = useState('')
|
||||
@@ -431,7 +431,7 @@ function SettingsTab({ city, onCityChange, onLogout }: { city: string; onCityCha
|
||||
<h2 style={{ fontSize: 24, fontWeight: 800, color: 'var(--text-primary)', margin: '0 0 8px', letterSpacing: '-0.5px' }}>Настройки</h2>
|
||||
|
||||
{/* City selector */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
<MapPin size={18} color="#818cf8" />
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Город</span>
|
||||
@@ -457,8 +457,35 @@ function SettingsTab({ city, onCityChange, onLogout }: { city: string; onCityCha
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Theme */}
|
||||
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 18 }}>{theme === 'dark' ? '🌙' : '☀️'}</span>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Тема</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{[
|
||||
{ id: 'dark', label: 'Тёмная' },
|
||||
{ id: 'light', label: 'Светлая' },
|
||||
].map(t => (
|
||||
<button key={t.id} onClick={() => onThemeChange(t.id)} style={{
|
||||
padding: '8px 16px', borderRadius: 12,
|
||||
background: theme === t.id ? 'rgba(99,102,241,0.12)' : 'rgba(255,255,255,0.02)',
|
||||
border: theme === t.id ? '1px solid rgba(129,140,248,0.25)' : '1px solid var(--card-border)',
|
||||
color: theme === t.id ? '#a5b4fc' : 'var(--text-secondary)',
|
||||
fontSize: 13, fontWeight: theme === t.id ? 600 : 500,
|
||||
transition: 'all 0.25s ease',
|
||||
}}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PIN change */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<KeyRound size={18} color="#818cf8" />
|
||||
@@ -551,8 +578,18 @@ function HomePageInner() {
|
||||
return 'spb'
|
||||
})
|
||||
const [screensaverActive, setScreensaverActive] = useState(false)
|
||||
const [theme, setTheme] = useState(() => {
|
||||
if (typeof window !== 'undefined') return localStorage.getItem('tablet-theme') || 'dark'
|
||||
return 'dark'
|
||||
})
|
||||
const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// Theme
|
||||
useEffect(() => {
|
||||
document.documentElement.className = theme
|
||||
localStorage.setItem('tablet-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
// Auth check
|
||||
useEffect(() => {
|
||||
fetch('/api/auth')
|
||||
@@ -690,7 +727,7 @@ function HomePageInner() {
|
||||
|
||||
{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} />
|
||||
<SettingsTab city={city} onCityChange={handleCityChange} onLogout={handleLogout} theme={theme} onThemeChange={setTheme} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
Reference in New Issue
Block a user