223 lines
8.0 KiB
TypeScript
223 lines
8.0 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';
|
|
|
|
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 [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);
|
|
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>
|
|
);
|
|
}
|