322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Moon, Volume2, Sun, Clock, MapPin, Thermometer } from 'lucide-react'
|
|
import {
|
|
getCurrentWeather,
|
|
setWeatherLocation,
|
|
setLocationHint,
|
|
getTempUnit,
|
|
setTempUnit,
|
|
getTimeFormat,
|
|
setTimeFormat,
|
|
type CurrentWeather,
|
|
type TempUnit,
|
|
type TimeFormat,
|
|
} from '@/lib/weatherApi'
|
|
|
|
export function SettingsPage() {
|
|
const [isDark, setIsDark] = useState(false)
|
|
const [volume, setVolume] = useState(70)
|
|
const [brightness, setBrightness] = useState(80)
|
|
const [timeout, setTimeout_] = useState(5)
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
// Weather location state
|
|
const [currentWeather, setCurrentWeather] = useState<CurrentWeather | null>(null)
|
|
const [weatherLoading, setWeatherLoading] = useState(true)
|
|
const [cityInput, setCityInput] = useState('')
|
|
const [savingCity, setSavingCity] = useState(false)
|
|
const [cityError, setCityError] = useState<string | null>(null)
|
|
const [citySuccess, setCitySuccess] = useState(false)
|
|
|
|
// Temperature unit state
|
|
const [tempUnit, setTempUnitState] = useState<TempUnit>('F')
|
|
|
|
// Time format state
|
|
const [timeFormatState, setTimeFormatState] = useState<TimeFormat>('12h')
|
|
|
|
useEffect(() => {
|
|
setMounted(true)
|
|
const isDarkMode = document.documentElement.classList.contains('dark')
|
|
setIsDark(isDarkMode)
|
|
|
|
// Load temperature unit preference
|
|
setTempUnitState(getTempUnit())
|
|
|
|
// Load time format preference
|
|
setTimeFormatState(getTimeFormat())
|
|
|
|
// Fetch current weather to show current city
|
|
getCurrentWeather()
|
|
.then((data) => setCurrentWeather(data))
|
|
.catch(() => {})
|
|
.finally(() => setWeatherLoading(false))
|
|
}, [])
|
|
|
|
const handleTempUnitChange = (unit: TempUnit) => {
|
|
setTempUnitState(unit)
|
|
setTempUnit(unit)
|
|
}
|
|
|
|
const handleTimeFormatChange = (format: TimeFormat) => {
|
|
setTimeFormatState(format)
|
|
setTimeFormat(format)
|
|
}
|
|
|
|
const toggleTheme = () => {
|
|
if (document.documentElement.classList.contains('dark')) {
|
|
document.documentElement.classList.remove('dark')
|
|
localStorage.setItem('theme', 'light')
|
|
setIsDark(false)
|
|
} else {
|
|
document.documentElement.classList.add('dark')
|
|
localStorage.setItem('theme', 'dark')
|
|
setIsDark(true)
|
|
}
|
|
}
|
|
|
|
const handleCitySubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!cityInput.trim()) return
|
|
|
|
setSavingCity(true)
|
|
setCityError(null)
|
|
setCitySuccess(false)
|
|
try {
|
|
await setWeatherLocation(cityInput.trim())
|
|
setLocationHint(true)
|
|
const data = await getCurrentWeather()
|
|
setCurrentWeather(data)
|
|
setCityInput('')
|
|
setCitySuccess(true)
|
|
// Clear success message after 3 seconds
|
|
setTimeout(() => setCitySuccess(false), 3000)
|
|
} catch (err) {
|
|
setCityError(err instanceof Error ? err.message : 'Failed to update location')
|
|
} finally {
|
|
setSavingCity(false)
|
|
}
|
|
}
|
|
|
|
if (!mounted) return null
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
{/* Header */}
|
|
<div className="bg-background border-b border-border px-6 py-4">
|
|
<h1 className="text-2xl font-bold">Settings</h1>
|
|
</div>
|
|
|
|
{/* Settings Content */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="space-y-6 max-w-md">
|
|
{/* Dark Mode Toggle */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
{isDark ? <Moon size={20} /> : <Sun size={20} />}
|
|
<div>
|
|
<div className="font-medium">Dark Mode</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{isDark ? 'Enabled' : 'Disabled'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={toggleTheme}
|
|
className={`
|
|
w-12 h-6 rounded-full transition-colors flex items-center px-1
|
|
${isDark ? 'bg-primary' : 'bg-muted'}
|
|
`}
|
|
>
|
|
<div
|
|
className={`
|
|
w-4 h-4 bg-white rounded-full transition-transform
|
|
${isDark ? 'translate-x-6' : 'translate-x-0'}
|
|
`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Volume Control */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<Volume2 size={20} />
|
|
<div className="flex-1">
|
|
<div className="font-medium">Volume</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{volume}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
value={volume}
|
|
onChange={(e) => setVolume(Number(e.target.value))}
|
|
className="w-full h-2 bg-muted cursor-pointer"
|
|
/>
|
|
</div>
|
|
|
|
{/* Brightness Control */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<Sun size={20} />
|
|
<div className="flex-1">
|
|
<div className="font-medium">Brightness</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{brightness}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
value={brightness}
|
|
onChange={(e) => setBrightness(Number(e.target.value))}
|
|
className="w-full h-2 bg-muted cursor-pointer"
|
|
/>
|
|
</div>
|
|
|
|
{/* Timeout Control */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<Clock size={20} />
|
|
<div className="flex-1">
|
|
<div className="font-medium">Auto-Lock Timeout</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{timeout} minutes
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<select
|
|
value={timeout}
|
|
onChange={(e) => setTimeout_(Number(e.target.value))}
|
|
className="w-full border border-border bg-input text-foreground p-2"
|
|
>
|
|
<option value={1}>1 minute</option>
|
|
<option value={3}>3 minutes</option>
|
|
<option value={5}>5 minutes</option>
|
|
<option value={10}>10 minutes</option>
|
|
<option value={15}>15 minutes</option>
|
|
<option value={30}>30 minutes</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Weather Location */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<MapPin size={20} />
|
|
<div className="flex-1">
|
|
<div className="font-medium">Weather Location</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{weatherLoading
|
|
? 'Loading...'
|
|
: currentWeather?.city || 'Not set'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form onSubmit={handleCitySubmit} className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={cityInput}
|
|
onChange={(e) => setCityInput(e.target.value)}
|
|
placeholder="Enter new city"
|
|
disabled={savingCity}
|
|
className="flex-1 border border-border bg-input text-foreground text-sm px-3 py-1.5"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={savingCity || !cityInput.trim()}
|
|
className="bg-primary text-primary-foreground text-sm px-3 py-1.5 disabled:opacity-50"
|
|
>
|
|
{savingCity ? 'Saving...' : 'Update'}
|
|
</button>
|
|
</form>
|
|
{cityError && (
|
|
<div className="text-xs text-negative">{cityError}</div>
|
|
)}
|
|
{citySuccess && (
|
|
<div className="text-xs text-positive">Location updated successfully</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Temperature Unit */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<Thermometer size={20} />
|
|
<div className="flex-1">
|
|
<div className="font-medium">Temperature Unit</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{tempUnit === 'C' ? 'Celsius' : 'Fahrenheit'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleTempUnitChange('F')}
|
|
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
|
tempUnit === 'F'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
}`}
|
|
>
|
|
Fahrenheit (°F)
|
|
</button>
|
|
<button
|
|
onClick={() => handleTempUnitChange('C')}
|
|
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
|
tempUnit === 'C'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
}`}
|
|
>
|
|
Celsius (°C)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time Format */}
|
|
<div className="border border-border bg-card p-4 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<Clock size={20} />
|
|
<div className="flex-1">
|
|
<div className="font-medium">Time Format</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{timeFormatState === '24h' ? '24-hour' : '12-hour'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleTimeFormatChange('12h')}
|
|
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
|
timeFormatState === '12h'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
}`}
|
|
>
|
|
12-hour
|
|
</button>
|
|
<button
|
|
onClick={() => handleTimeFormatChange('24h')}
|
|
className={`flex-1 py-2 text-sm font-medium transition-colors ${
|
|
timeFormatState === '24h'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
}`}
|
|
>
|
|
24-hour
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|