Files
smart-home-tablet/components/SpotifyWidget.tsx
d.klimov 2755980165
Some checks failed
Deploy / deploy (push) Has been cancelled
design: красивые пустые состояния таймеров/Spotify, акцентный прогноз
- таймеры: светящаяся градиентная иконка + голосовая подсказка вместо серого текста
- Spotify: зелёный музыкальный орб + подсказка
- прогноз: ячейка «сейчас» — яркий градиентный акцент с тенью
2026-07-22 23:25:21 +03:00

140 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect, useCallback } from 'react'
import { SkipBack, SkipForward, Play, Pause, Music } from 'lucide-react'
interface SpotifyState {
playing: boolean
track?: string
artist?: string
album?: string
volume?: number
device?: string
}
export default function SpotifyWidget() {
const [state, setState] = useState<SpotifyState | null>(null)
const [loading, setLoading] = useState(true)
const [actionLoading, setActionLoading] = useState(false)
const fetchState = useCallback(async () => {
try {
const r = await fetch('/api/voice/tools/spotify', {
headers: {
'x-voice-internal': process.env.NEXT_PUBLIC_VOICE_API_KEY || '',
},
})
if (r.ok) {
const d = await r.json()
setState(d)
}
} catch {}
finally { setLoading(false) }
}, [])
useEffect(() => {
fetchState()
const t = setInterval(fetchState, 15_000)
return () => clearInterval(t)
}, [fetchState])
const control = async (action: string) => {
setActionLoading(true)
try {
await fetch('/api/voice/tools/spotify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-voice-internal': process.env.NEXT_PUBLIC_VOICE_API_KEY || '',
},
body: JSON.stringify({ action }),
})
setTimeout(fetchState, 500)
} catch {}
finally { setActionLoading(false) }
}
const btnStyle = (active?: boolean): React.CSSProperties => ({
width: 40, height: 40, borderRadius: 12,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: active ? 'rgba(30,215,96,0.15)' : 'rgba(255,255,255,0.04)',
border: `1px solid ${active ? 'rgba(30,215,96,0.3)' : 'rgba(255,255,255,0.07)'}`,
color: active ? '#1ED760' : 'var(--text-secondary)',
transition: 'all 0.2s ease',
cursor: actionLoading ? 'not-allowed' : 'pointer',
opacity: actionLoading ? 0.6 : 1,
})
return (
<div className="card" style={{ padding: '16px 20px', display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 28, height: 28, borderRadius: 8,
background: 'rgba(30,215,96,0.12)',
border: '1px solid rgba(30,215,96,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<Music size={14} color="#1ED760" />
</div>
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-secondary)' }}>Spotify</span>
{state?.device && (
<span style={{ fontSize: 10, color: 'var(--text-tertiary)', marginLeft: 'auto' }}>
{state.device}
</span>
)}
</div>
{/* Track info */}
{loading ? (
<div style={{ fontSize: 13, color: 'var(--text-tertiary)', textAlign: 'center', padding: '8px 0' }}>Загрузка...</div>
) : !state?.track ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, padding: '18px 0' }}>
<div style={{ position: 'relative', width: 78, height: 78, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{
position: 'absolute', inset: 0, borderRadius: '50%',
background: 'radial-gradient(circle, rgba(30,215,96,0.38), transparent 70%)', filter: 'blur(7px)',
}} />
<div style={{
position: 'absolute', inset: 12, borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(30,215,96,0.24), rgba(30,215,96,0.08))',
border: '1px solid rgba(30,215,96,0.32)',
}} />
<Music size={32} color="#1ED760" style={{ position: 'relative' }} />
</div>
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)' }}>Ничего не играет</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', textAlign: 'center' }}>Скажи «Эй, космо, включи что-нибудь»</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div style={{
fontSize: 14, fontWeight: 700, color: 'var(--text-primary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{state.track}</div>
<div style={{
fontSize: 12, color: 'var(--text-secondary)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{state.artist}</div>
</div>
)}
{/* Controls */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
<button style={btnStyle()} onClick={() => control('previous')} disabled={actionLoading}>
<SkipBack size={16} />
</button>
<button
style={{ ...btnStyle(state?.playing), width: 48, height: 48, borderRadius: 14 }}
onClick={() => control(state?.playing ? 'pause' : 'play')}
disabled={actionLoading}
>
{state?.playing ? <Pause size={18} /> : <Play size={18} />}
</button>
<button style={btnStyle()} onClick={() => control('next')} disabled={actionLoading}>
<SkipForward size={16} />
</button>
</div>
</div>
)
}