toes/src/client/components/SettingsPage.tsx

308 lines
9.4 KiB
TypeScript

import { useEffect, useState } from 'hono/jsx'
import { connectToWifi, getWifiStatus, scanWifiNetworks } from '../api'
import { setCurrentView, setupMode } from '../state'
import {
Button,
DashboardInstallCmd,
ErrorBox,
FormActions,
FormField,
FormInput,
FormLabel,
HeaderActions,
InfoLabel,
InfoRow,
InfoValue,
Main,
MainContent,
MainHeader,
MainTitle,
NetworkItem,
NetworkListWrap,
NetworkMeta,
NetworkName,
Section,
SectionTitle,
SignalBarSegment,
SignalBarsWrap,
Spinner,
SpinnerWrap,
SuccessCheck,
WifiColumn,
} from '../styles'
import { theme } from '../themes'
import type { WifiNetwork } from '../../shared/types'
type WifiStep = 'status' | 'scanning' | 'networks' | 'password' | 'connecting' | 'success'
function signalBars(signal: number) {
const level = signal > 75 ? 4 : signal > 50 ? 3 : signal > 25 ? 2 : 1
return (
<SignalBarsWrap>
{[1, 2, 3, 4].map(i => (
<SignalBarSegment
key={i}
level={i <= level ? 'active' : 'inactive'}
style={{ height: 3 + i * 3 }}
/>
))}
</SignalBarsWrap>
)
}
function NetworkList({ networks, onSelect }: { networks: WifiNetwork[], onSelect: (net: WifiNetwork) => void }) {
if (networks.length === 0) {
return (
<SpinnerWrap>
No networks found. Try scanning again.
</SpinnerWrap>
)
}
return (
<NetworkListWrap>
{networks.map(net => (
<NetworkItem key={net.ssid} onClick={() => onSelect(net)}>
<NetworkName>{net.ssid}</NetworkName>
<NetworkMeta>
{net.security && net.security !== '' && net.security !== '--' && <span style={{ fontSize: 12 }}>🔒</span>}
{signalBars(net.signal)}
</NetworkMeta>
</NetworkItem>
))}
</NetworkListWrap>
)
}
export function SettingsPage({ render }: { render: () => void }) {
const [step, setStep] = useState<WifiStep>('status')
const [connected, setConnected] = useState(false)
const [currentSsid, setCurrentSsid] = useState('')
const [currentIp, setCurrentIp] = useState('')
const [networks, setNetworks] = useState<WifiNetwork[]>([])
const [selectedNetwork, setSelectedNetwork] = useState<WifiNetwork | null>(null)
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [successSsid, setSuccessSsid] = useState('')
const [successIp, setSuccessIp] = useState('')
const [serverUrl, setServerUrl] = useState('')
const fetchStatus = () => {
getWifiStatus().then(status => {
setConnected(status.connected)
setCurrentSsid(status.ssid)
setCurrentIp(status.ip)
if (status.url) setServerUrl(status.url)
}).catch(() => {})
}
useEffect(() => {
fetchStatus()
if (setupMode) doScan()
}, [])
const goBack = () => {
setCurrentView('dashboard')
render()
}
const doScan = () => {
setStep('scanning')
setError('')
scanWifiNetworks()
.then(nets => {
setNetworks(nets)
setStep('networks')
})
.catch(() => {
setError('Failed to scan networks')
setStep('networks')
})
}
const handleSelectNetwork = (net: WifiNetwork) => {
setSelectedNetwork(net)
setPassword('')
setError('')
const needsPassword = net.security && net.security !== '' && net.security !== '--'
if (needsPassword) {
setStep('password')
} else {
doConnect(net.ssid)
}
}
const doConnect = (ssid: string, pw?: string) => {
setStep('connecting')
setError('')
connectToWifi(ssid, pw)
.then(result => {
if (result.ok) {
setSuccessSsid(result.ssid || ssid)
setSuccessIp(result.ip || '')
setStep('success')
fetchStatus()
} else {
setError(result.error || 'Connection failed. Check your password and try again.')
setStep('password')
}
})
.catch(() => {
setError('Connection failed. Please try again.')
setStep('password')
})
}
const handleConnect = (e: Event) => {
e.preventDefault()
if (!selectedNetwork) return
doConnect(selectedNetwork.ssid, password || undefined)
}
const title = setupMode ? 'WiFi Setup' : 'Settings'
return (
<Main>
<MainHeader centered>
<MainTitle>{title}</MainTitle>
{!setupMode && (
<HeaderActions>
<Button onClick={goBack}>Back</Button>
</HeaderActions>
)}
</MainHeader>
<MainContent centered>
<Section>
<SectionTitle>WiFi</SectionTitle>
{/* Status display */}
{step === 'status' && (
<WifiColumn>
<div>
<InfoRow>
<InfoLabel>Status</InfoLabel>
<InfoValue style={{ color: connected ? theme('colors-statusRunning') : theme('colors-error'), fontWeight: 500 }}>
{connected ? 'Connected' : 'Disconnected'}
</InfoValue>
</InfoRow>
{connected && currentSsid && (
<InfoRow>
<InfoLabel>Network</InfoLabel>
<InfoValue>{currentSsid}</InfoValue>
</InfoRow>
)}
{connected && currentIp && (
<InfoRow>
<InfoLabel>IP</InfoLabel>
<InfoValue style={{ fontFamily: theme('fonts-mono') }}>{currentIp}</InfoValue>
</InfoRow>
)}
</div>
<FormActions>
<Button onClick={doScan}>Scan Networks</Button>
</FormActions>
</WifiColumn>
)}
{/* Scanning spinner */}
{step === 'scanning' && (
<SpinnerWrap>
<Spinner />
<p style={{ color: theme('colors-textMuted') }}>Scanning for networks...</p>
</SpinnerWrap>
)}
{/* Network list */}
{step === 'networks' && (
<WifiColumn style={{ gap: 12 }}>
<NetworkList networks={networks} onSelect={handleSelectNetwork} />
{error && <ErrorBox>{error}</ErrorBox>}
<FormActions>
{!setupMode && <Button onClick={() => setStep('status')}>Back</Button>}
<Button onClick={doScan}>Rescan</Button>
</FormActions>
</WifiColumn>
)}
{/* Password entry */}
{step === 'password' && (
<WifiColumn>
<FormField>
<FormLabel>Network</FormLabel>
<div style={{ fontWeight: 500, fontSize: 16 }}>{selectedNetwork?.ssid}</div>
</FormField>
<form onSubmit={handleConnect} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<FormField>
<FormLabel>Password</FormLabel>
<FormInput
type="password"
value={password}
onInput={(e: Event) => setPassword((e.target as HTMLInputElement).value)}
placeholder="Enter WiFi password"
autofocus
/>
</FormField>
{error && <ErrorBox>{error}</ErrorBox>}
<FormActions>
<Button onClick={() => { setError(''); setStep('networks') }}>Back</Button>
<Button variant="primary" type="submit">Connect</Button>
</FormActions>
</form>
</WifiColumn>
)}
{/* Connecting spinner */}
{step === 'connecting' && (
<SpinnerWrap>
<Spinner />
<p style={{ color: theme('colors-textMuted') }}>Connecting to <strong>{selectedNetwork?.ssid}</strong>...</p>
</SpinnerWrap>
)}
{/* Success */}
{step === 'success' && (
<WifiColumn style={{ textAlign: 'center' }}>
<SuccessCheck></SuccessCheck>
<h3 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Connected!</h3>
<p style={{ color: theme('colors-textMuted'), marginBottom: 16 }}>
Connected to <strong>{successSsid}</strong>
{successIp && <span> ({successIp})</span>}
</p>
{setupMode ? (
<div style={{
marginTop: 20,
padding: 16,
background: 'var(--colors-bgSubtle)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--colors-border)',
}}>
<p style={{ marginBottom: 8 }}>Reconnect your device to <strong>{successSsid}</strong> and visit:</p>
<a
href={serverUrl}
style={{ color: theme('colors-statusRunning'), fontSize: 18, fontWeight: 600, textDecoration: 'none' }}
>
{serverUrl}
</a>
</div>
) : (
<FormActions>
<Button onClick={() => { fetchStatus(); setStep('status') }}>Done</Button>
</FormActions>
)}
</WifiColumn>
)}
</Section>
{!setupMode && (
<Section>
<SectionTitle>Install CLI</SectionTitle>
<DashboardInstallCmd>
curl -fsSL {location.origin}/install | bash
</DashboardInstallCmd>
</Section>
)}
</MainContent>
</Main>
)
}