Files
RFID/components/ConnectionPanel.tsx
2026-01-23 11:41:59 +09:00

121 lines
4.4 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { ConnectionStatus } from '../types';
import { Wifi, WifiOff, Activity } from 'lucide-react';
interface Props {
status: ConnectionStatus;
onConnect: () => void;
onDisconnect: () => void;
baudRate: number;
setBaudRate: (rate: number) => void;
address: number;
setAddress: (addr: number) => void;
}
export const ConnectionPanel: React.FC<Props> = ({
status,
onConnect,
onDisconnect,
baudRate,
setBaudRate,
address,
setAddress
}) => {
const isConnected = status === ConnectionStatus.CONNECTED;
// Local state for address input to allow smooth typing without auto-formatting interference
const [localAddress, setLocalAddress] = useState(address.toString(16).toUpperCase().padStart(2, '0'));
const [isFocused, setIsFocused] = useState(false);
// Sync from props only when not editing
useEffect(() => {
if (!isFocused) {
setLocalAddress(address.toString(16).toUpperCase().padStart(2, '0'));
}
}, [address, isFocused]);
const handleAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value.toUpperCase();
// Validate Hex and Length
if (/^[0-9A-F]*$/.test(val) && val.length <= 2) {
setLocalAddress(val);
// Update parent state if valid
if (val !== '') {
setAddress(parseInt(val, 16));
}
}
};
const handleBlur = () => {
setIsFocused(false);
// On blur, ensure the display matches the formatted prop value (pads single digits, etc.)
setLocalAddress(address.toString(16).toUpperCase().padStart(2, '0'));
};
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
<div className="flex items-center gap-2 mb-4">
{isConnected ? <Wifi className="text-emerald-500" /> : <WifiOff className="text-slate-400" />}
<h2 className="text-lg font-semibold text-slate-800">Connection</h2>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Device Model</label>
<select
disabled={isConnected}
className="w-full bg-slate-50 border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 disabled:opacity-50"
>
<option value="CR100"> CR-100</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Baud Rate</label>
<select
disabled={isConnected}
value={baudRate}
onChange={(e) => setBaudRate(Number(e.target.value))}
className="w-full bg-slate-50 border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 disabled:opacity-50"
>
<option value={9600}>9600</option>
<option value={19200}>19200</option>
<option value={38400}>38400</option>
<option value={57600}>57600 (Default)</option>
<option value={115200}>115200</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Reader Address (Hex)</label>
<input
type="text"
value={localAddress}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
onChange={handleAddressChange}
className="w-full bg-slate-50 border border-slate-300 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 font-mono"
maxLength={2}
placeholder="FF"
/>
</div>
<button
onClick={isConnected ? onDisconnect : onConnect}
className={`w-full flex items-center justify-center gap-2 font-medium rounded-lg text-sm px-5 py-2.5 transition-colors ${
isConnected
? 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200'
: 'bg-blue-600 text-white hover:bg-blue-700 shadow-md shadow-blue-500/20'
}`}
>
{isConnected ? 'Disconnect' : 'Connect Serial'}
{status === ConnectionStatus.CONNECTING && <Activity className="animate-spin h-4 w-4" />}
</button>
<div className="text-xs text-slate-400 text-center">
{status}
</div>
</div>
</div>
);
};