initial commit
This commit is contained in:
121
components/ConnectionPanel.tsx
Normal file
121
components/ConnectionPanel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user