meatloaf-config/src/app/components/WiFiScanOverlay.tsx

229 lines
8.2 KiB
TypeScript

import { useState, useEffect } from 'react';
import { X, Wifi, Lock, Signal } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { toast } from 'sonner';
import { useWs } from '../ws';
interface WiFiNetwork {
ssid: string;
signal: number;
secured: boolean;
}
interface WiFiScanOverlayProps {
onClose: () => void;
onNetworkAdded: (ssid: string, passphrase: string) => void;
existingNetworks: string[];
}
export default function WiFiScanOverlay({ onClose, onNetworkAdded, existingNetworks }: WiFiScanOverlayProps) {
const { send: wsSend } = useWs();
const [isScanning, setIsScanning] = useState(true);
const [networks, setNetworks] = useState<WiFiNetwork[]>([]);
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork | null>(null);
const [password, setPassword] = useState('');
const [isConnecting, setIsConnecting] = useState(false);
useEffect(() => {
// Simulate network scan
const scanNetworks = async () => {
await new Promise(resolve => setTimeout(resolve, 2000));
const mockNetworks: WiFiNetwork[] = [
{ ssid: 'HomeNetwork-5G', signal: 95, secured: true },
{ ssid: 'CoffeeShop_WiFi', signal: 88, secured: false },
{ ssid: 'Neighbor-WiFi', signal: 72, secured: true },
{ ssid: 'Office-Guest', signal: 65, secured: false },
{ ssid: 'Mobile_Hotspot', signal: 58, secured: true },
{ ssid: 'Library_Public', signal: 45, secured: false },
{ ssid: 'Hidden-Network', signal: 30, secured: true }
].filter(net => !existingNetworks.includes(net.ssid));
setNetworks(mockNetworks);
setIsScanning(false);
};
scanNetworks();
}, [existingNetworks]);
const handleConnect = async () => {
if (!selectedNetwork) return;
if (selectedNetwork.secured && !password) {
toast.error('Password required for secured network');
return;
}
setIsConnecting(true);
const cmd = selectedNetwork.secured
? `connect ${selectedNetwork.ssid} ${password}`
: `connect ${selectedNetwork.ssid}`;
wsSend(cmd);
toast.loading('Connecting to network...');
// Simulate connection
await new Promise(resolve => setTimeout(resolve, 1500));
toast.dismiss();
toast.success(`Connected to ${selectedNetwork.ssid}`);
onNetworkAdded(selectedNetwork.ssid, password);
onClose();
};
const getSignalIcon = (signal: number) => {
if (signal > 80) return 'text-green-600';
if (signal > 60) return 'text-yellow-600';
return 'text-red-600';
};
const getSignalBars = (signal: number) => {
const bars = Math.ceil(signal / 25);
return (
<div className="flex items-end gap-0.5 h-4">
{[1, 2, 3, 4].map((bar) => (
<div
key={bar}
className={`w-1 ${bar <= bars ? 'bg-current' : 'bg-neutral-300'}`}
style={{ height: `${bar * 25}%` }}
/>
))}
</div>
);
};
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-50"
onClick={onClose}
>
<motion.div
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className="absolute inset-0 bg-white overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-white border-b border-neutral-200 z-10">
<div className="flex items-center justify-between p-4">
<button onClick={onClose} className="p-2 -m-2">
<X className="w-6 h-6" />
</button>
<h2>WiFi Networks</h2>
<div className="w-10" />
</div>
</div>
{isScanning ? (
<div className="flex flex-col items-center justify-center py-20">
<div className="relative w-16 h-16 mb-4">
<motion.div
animate={{ scale: [1, 1.2, 1], opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="absolute inset-0 bg-blue-500 rounded-full"
/>
<div className="absolute inset-0 flex items-center justify-center">
<Wifi className="w-8 h-8 text-white z-10" />
</div>
</div>
<div className="text-neutral-600">Scanning for networks...</div>
<div className="text-sm text-neutral-500 mt-1">Please wait</div>
</div>
) : selectedNetwork ? (
<div className="p-4">
<div className="bg-white border border-neutral-200 rounded-lg p-4 mb-4">
<div className="flex items-center gap-3 mb-4">
<div className={`${getSignalIcon(selectedNetwork.signal)}`}>
{getSignalBars(selectedNetwork.signal)}
</div>
<div className="flex-1">
<div className="font-medium">{selectedNetwork.ssid}</div>
<div className="text-sm text-neutral-500">
Signal: {selectedNetwork.signal}%
</div>
</div>
{selectedNetwork.secured && (
<Lock className="w-5 h-5 text-neutral-400" />
)}
</div>
{selectedNetwork.secured && (
<div>
<label className="text-sm text-neutral-500 block mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter network password"
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
autoFocus
/>
</div>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => {
setSelectedNetwork(null);
setPassword('');
}}
className="flex-1 py-2 px-4 border border-neutral-300 text-neutral-700 rounded-lg"
>
Back
</button>
<button
onClick={handleConnect}
disabled={isConnecting}
className="flex-1 py-2 px-4 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isConnecting ? 'Connecting...' : 'Connect'}
</button>
</div>
</div>
) : (
<div className="p-4">
<div className="text-sm text-neutral-500 mb-3">
{networks.length} network{networks.length !== 1 ? 's' : ''} found
</div>
<div className="space-y-2">
{networks.map((network, index) => (
<button
key={index}
onClick={() => setSelectedNetwork(network)}
className="w-full bg-white border border-neutral-200 rounded-lg p-4 flex items-center gap-3 hover:bg-neutral-50"
>
<div className={getSignalIcon(network.signal)}>
{getSignalBars(network.signal)}
</div>
<div className="flex-1 text-left">
<div className="font-medium">{network.ssid}</div>
<div className="text-sm text-neutral-500">
Signal: {network.signal}%
</div>
</div>
{network.secured && (
<Lock className="w-5 h-5 text-neutral-400" />
)}
</button>
))}
</div>
{networks.length === 0 && (
<div className="text-center py-8 text-neutral-500">
No new networks found
</div>
)}
</div>
)}
</motion.div>
</motion.div>
</AnimatePresence>
);
}