@mytec: iteration1 implemented
This commit is contained in:
@@ -27,7 +27,7 @@ export default function FrequencySelector({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-dark-text">
|
||||
Operating Frequency
|
||||
</label>
|
||||
|
||||
@@ -38,17 +38,17 @@ export default function FrequencySelector({
|
||||
key={freq}
|
||||
type="button"
|
||||
onClick={() => onChange(freq)}
|
||||
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors min-h-[32px]
|
||||
${
|
||||
value === freq
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
? 'bg-blue-600 text-white dark:bg-blue-500'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-dark-border dark:text-dark-text dark:hover:bg-dark-muted'
|
||||
}`}
|
||||
>
|
||||
{freq}
|
||||
</button>
|
||||
))}
|
||||
<span className="self-center text-xs text-gray-400">MHz</span>
|
||||
<span className="self-center text-xs text-gray-400 dark:text-dark-muted">MHz</span>
|
||||
</div>
|
||||
|
||||
{/* Custom input */}
|
||||
@@ -59,7 +59,7 @@ export default function FrequencySelector({
|
||||
value={customInput}
|
||||
onChange={(e) => setCustomInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCustomSubmit()}
|
||||
className="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm
|
||||
className="flex-1 px-3 py-1.5 border border-gray-300 dark:border-dark-border dark:bg-dark-bg dark:text-dark-text rounded-md text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
min={1}
|
||||
max={100000}
|
||||
@@ -67,32 +67,32 @@ export default function FrequencySelector({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCustomSubmit}
|
||||
className="px-3 py-1.5 bg-gray-200 hover:bg-gray-300 rounded-md text-sm text-gray-700"
|
||||
className="px-3 py-1.5 bg-gray-200 hover:bg-gray-300 dark:bg-dark-border dark:hover:bg-dark-muted dark:text-dark-text rounded-md text-sm text-gray-700 min-h-[32px]"
|
||||
>
|
||||
Set
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Current frequency display */}
|
||||
<div className="text-sm text-gray-600 font-medium">
|
||||
<div className="text-sm text-gray-600 dark:text-dark-muted font-medium">
|
||||
Current: {value} MHz
|
||||
</div>
|
||||
|
||||
{/* Band info */}
|
||||
{bandInfo ? (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-2 text-xs space-y-0.5">
|
||||
<div className="font-semibold text-blue-700">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-2 text-xs space-y-0.5">
|
||||
<div className="font-semibold text-blue-700 dark:text-blue-300">
|
||||
{bandInfo.name} ({bandInfo.range})
|
||||
</div>
|
||||
<div className="text-blue-600">
|
||||
<div className="text-blue-600 dark:text-blue-400">
|
||||
λ = {getWavelength(value)} · {bandInfo.characteristics.range} range ·{' '}
|
||||
{bandInfo.characteristics.penetration} penetration
|
||||
</div>
|
||||
<div className="text-blue-500">{bandInfo.characteristics.typical}</div>
|
||||
<div className="text-blue-500 dark:text-blue-400">{bandInfo.characteristics.typical}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-md p-2 text-xs">
|
||||
<div className="text-gray-600">
|
||||
<div className="bg-gray-50 dark:bg-dark-bg border border-gray-200 dark:border-dark-border rounded-md p-2 text-xs">
|
||||
<div className="text-gray-600 dark:text-dark-muted">
|
||||
Custom frequency: {value} MHz · λ = {getWavelength(value)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,36 @@ interface SiteFormProps {
|
||||
pendingLocation?: { lat: number; lon: number } | null;
|
||||
onClose: () => void;
|
||||
onClearPending?: () => void;
|
||||
onCalculate?: () => void;
|
||||
}
|
||||
|
||||
const TEMPLATES = {
|
||||
limesdr: { name: 'LimeSDR', power: 20, gain: 3, frequency: 1800, height: 5 },
|
||||
lowBBU: { name: 'Low BBU', power: 37, gain: 15, frequency: 1800, height: 25 },
|
||||
highBBU: { name: 'High BBU', power: 46, gain: 18, frequency: 1800, height: 35 },
|
||||
limesdr: {
|
||||
name: 'LimeSDR Mini',
|
||||
power: 20,
|
||||
gain: 2,
|
||||
frequency: 1800,
|
||||
height: 10,
|
||||
antennaType: 'omni' as const,
|
||||
},
|
||||
lowBBU: {
|
||||
name: 'Low Power BBU',
|
||||
power: 40,
|
||||
gain: 8,
|
||||
frequency: 1800,
|
||||
height: 20,
|
||||
antennaType: 'omni' as const,
|
||||
},
|
||||
highBBU: {
|
||||
name: 'High Power BBU',
|
||||
power: 43,
|
||||
gain: 15,
|
||||
frequency: 1800,
|
||||
height: 30,
|
||||
antennaType: 'sector' as const,
|
||||
azimuth: 0,
|
||||
beamwidth: 65,
|
||||
},
|
||||
};
|
||||
|
||||
export default function SiteForm({
|
||||
@@ -25,9 +49,11 @@ export default function SiteForm({
|
||||
pendingLocation,
|
||||
onClose,
|
||||
onClearPending,
|
||||
onCalculate,
|
||||
}: SiteFormProps) {
|
||||
const addSite = useSitesStore((s) => s.addSite);
|
||||
const updateSite = useSitesStore((s) => s.updateSite);
|
||||
const deleteSite = useSitesStore((s) => s.deleteSite);
|
||||
const addToast = useToastStore((s) => s.addToast);
|
||||
|
||||
const [name, setName] = useState(editSite?.name ?? 'Station-1');
|
||||
@@ -58,9 +84,13 @@ export default function SiteForm({
|
||||
setGain(t.gain);
|
||||
setFrequency(t.frequency);
|
||||
setHeight(t.height);
|
||||
setAntennaType(t.antennaType);
|
||||
if ('azimuth' in t) setAzimuth(t.azimuth);
|
||||
if ('beamwidth' in t) setBeamwidth(t.beamwidth);
|
||||
addToast(`Applied: ${t.name}`, 'success');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleSave = async () => {
|
||||
if (editSite) {
|
||||
await updateSite(editSite.id, {
|
||||
name,
|
||||
@@ -98,15 +128,33 @@ export default function SiteForm({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSaveAndCalculate = async () => {
|
||||
await handleSave();
|
||||
// Trigger calculation after save
|
||||
if (onCalculate) {
|
||||
// Small delay to ensure store is updated
|
||||
setTimeout(() => onCalculate(), 50);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (editSite) {
|
||||
await deleteSite(editSite.id);
|
||||
addToast(`"${editSite.name}" deleted`, 'info');
|
||||
onClearPending?.();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow-lg overflow-y-auto max-h-[calc(100vh-120px)]">
|
||||
<div className="sticky top-0 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-800">
|
||||
<div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-lg overflow-y-auto max-h-[calc(100vh-120px)]">
|
||||
<div className="sticky top-0 bg-white dark:bg-dark-surface border-b border-gray-200 dark:border-dark-border px-4 py-3 flex items-center justify-between z-10">
|
||||
<h2 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
|
||||
{editSite ? 'Edit Site' : 'New Site Configuration'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 text-lg"
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-dark-text text-lg min-w-[32px] min-h-[32px] flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -123,7 +171,7 @@ export default function SiteForm({
|
||||
|
||||
{/* Coordinates */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-gray-700">Coordinates</label>
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-dark-text">Coordinates</label>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
@@ -131,7 +179,7 @@ export default function SiteForm({
|
||||
step="0.0001"
|
||||
value={lat}
|
||||
onChange={(e) => setLat(Number(e.target.value))}
|
||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm
|
||||
className="w-full px-3 py-1.5 border border-gray-300 dark:border-dark-border dark:bg-dark-bg dark:text-dark-text rounded-md text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Lat"
|
||||
/>
|
||||
@@ -142,7 +190,7 @@ export default function SiteForm({
|
||||
step="0.0001"
|
||||
value={lon}
|
||||
onChange={(e) => setLon(Number(e.target.value))}
|
||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm
|
||||
className="w-full px-3 py-1.5 border border-gray-300 dark:border-dark-border dark:bg-dark-bg dark:text-dark-text rounded-md text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Lon"
|
||||
/>
|
||||
@@ -151,8 +199,8 @@ export default function SiteForm({
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-gray-200 pt-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
<div className="border-t border-gray-200 dark:border-dark-border pt-2">
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-dark-muted uppercase tracking-wide">
|
||||
RF Parameters
|
||||
</span>
|
||||
</div>
|
||||
@@ -183,8 +231,8 @@ export default function SiteForm({
|
||||
<FrequencySelector value={frequency} onChange={setFrequency} />
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-gray-200 pt-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
<div className="border-t border-gray-200 dark:border-dark-border pt-2">
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-dark-muted uppercase tracking-wide">
|
||||
Physical Parameters
|
||||
</span>
|
||||
</div>
|
||||
@@ -202,11 +250,11 @@ export default function SiteForm({
|
||||
|
||||
{/* Antenna type */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-dark-text">
|
||||
Antenna Type
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<label className="flex items-center gap-2 cursor-pointer min-h-[44px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="antennaType"
|
||||
@@ -214,9 +262,9 @@ export default function SiteForm({
|
||||
onChange={() => setAntennaType('omni')}
|
||||
className="accent-blue-600"
|
||||
/>
|
||||
<span className="text-sm">Omni</span>
|
||||
<span className="text-sm dark:text-dark-text">Omni</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<label className="flex items-center gap-2 cursor-pointer min-h-[44px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="antennaType"
|
||||
@@ -224,14 +272,14 @@ export default function SiteForm({
|
||||
onChange={() => setAntennaType('sector')}
|
||||
className="accent-blue-600"
|
||||
/>
|
||||
<span className="text-sm">Sector</span>
|
||||
<span className="text-sm dark:text-dark-text">Sector</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sector parameters - collapsible */}
|
||||
{antennaType === 'sector' && (
|
||||
<div className="bg-gray-50 rounded-md p-3 space-y-3 border border-gray-200">
|
||||
<div className="bg-gray-50 dark:bg-dark-bg rounded-md p-3 space-y-3 border border-gray-200 dark:border-dark-border">
|
||||
<Slider
|
||||
label="Azimuth (degrees)"
|
||||
value={azimuth}
|
||||
@@ -252,15 +300,15 @@ export default function SiteForm({
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-gray-200 pt-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
<div className="border-t border-gray-200 dark:border-dark-border pt-2">
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-dark-muted uppercase tracking-wide">
|
||||
Notes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-dark-text">
|
||||
Equipment / Notes (optional)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -268,55 +316,61 @@ export default function SiteForm({
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="e.g., ZTE B8200 BBU + custom omni antenna"
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-dark-border dark:bg-dark-bg dark:text-dark-text rounded-md text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
||||
resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Templates */}
|
||||
{!editSite && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
Quick Templates
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyTemplate('limesdr')}
|
||||
className="px-2 py-1 bg-purple-100 hover:bg-purple-200 text-purple-700
|
||||
rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
LimeSDR
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyTemplate('lowBBU')}
|
||||
className="px-2 py-1 bg-green-100 hover:bg-green-200 text-green-700
|
||||
rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
Low BBU
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyTemplate('highBBU')}
|
||||
className="px-2 py-1 bg-orange-100 hover:bg-orange-200 text-orange-700
|
||||
rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
High BBU
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 dark:text-dark-muted uppercase tracking-wide">
|
||||
Quick Templates
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyTemplate('limesdr')}
|
||||
className="px-3 py-1.5 bg-purple-100 hover:bg-purple-200 text-purple-700
|
||||
dark:bg-purple-900/30 dark:hover:bg-purple-900/50 dark:text-purple-300
|
||||
rounded text-xs font-medium transition-colors min-h-[32px]"
|
||||
>
|
||||
LimeSDR
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyTemplate('lowBBU')}
|
||||
className="px-3 py-1.5 bg-green-100 hover:bg-green-200 text-green-700
|
||||
dark:bg-green-900/30 dark:hover:bg-green-900/50 dark:text-green-300
|
||||
rounded text-xs font-medium transition-colors min-h-[32px]"
|
||||
>
|
||||
Low BBU
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyTemplate('highBBU')}
|
||||
className="px-3 py-1.5 bg-orange-100 hover:bg-orange-200 text-orange-700
|
||||
dark:bg-orange-900/30 dark:hover:bg-orange-900/50 dark:text-orange-300
|
||||
rounded text-xs font-medium transition-colors min-h-[32px]"
|
||||
>
|
||||
High BBU
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button onClick={handleSubmit} className="flex-1">
|
||||
{editSite ? 'Save Changes' : 'Add Site'}
|
||||
<Button onClick={handleSaveAndCalculate} className="flex-1">
|
||||
Save & Calculate
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
<Button variant="secondary" onClick={handleSave}>
|
||||
Save Only
|
||||
</Button>
|
||||
{editSite && (
|
||||
<Button variant="danger" onClick={handleDelete}>
|
||||
Del
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,9 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
|
||||
<div className="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-800">
|
||||
<div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm">
|
||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-dark-border flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
|
||||
Sites ({sites.length})
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
@@ -43,31 +43,36 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
|
||||
</div>
|
||||
|
||||
{sites.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-400">
|
||||
<div className="p-4 text-center text-sm text-gray-400 dark:text-dark-muted">
|
||||
No sites yet. Click on the map or use "+ Manual" to add one.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100 max-h-60 overflow-y-auto">
|
||||
<div className="divide-y divide-gray-100 dark:divide-dark-border max-h-60 overflow-y-auto">
|
||||
{sites.map((site) => (
|
||||
<div
|
||||
key={site.id}
|
||||
className={`px-4 py-2 flex items-center gap-3 cursor-pointer hover:bg-gray-50
|
||||
transition-colors ${selectedSiteId === site.id ? 'bg-blue-50' : ''}`}
|
||||
className={`px-4 py-2.5 flex items-center gap-3 cursor-pointer
|
||||
hover:bg-gray-50 dark:hover:bg-dark-border/50 transition-colors
|
||||
${selectedSiteId === site.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''}`}
|
||||
onClick={() => selectSite(site.id)}
|
||||
>
|
||||
{/* Color indicator */}
|
||||
<div
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
className="w-4 h-4 rounded-full flex-shrink-0 border-2 border-white dark:border-dark-border shadow"
|
||||
style={{ backgroundColor: site.color }}
|
||||
/>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-800 truncate">
|
||||
<div className="text-sm font-medium text-gray-800 dark:text-dark-text truncate">
|
||||
{site.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{site.frequency} MHz · {site.power} dBm · {site.antennaType}
|
||||
<div className="text-xs text-gray-500 dark:text-dark-muted">
|
||||
{site.frequency} MHz · {site.power} dBm ·{' '}
|
||||
{site.antennaType === 'omni'
|
||||
? 'Omni'
|
||||
: `Sector ${site.azimuth ?? 0}°`}
|
||||
{' '}· {site.height}m
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +83,7 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
|
||||
e.stopPropagation();
|
||||
onEditSite(site);
|
||||
}}
|
||||
className="px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 rounded"
|
||||
className="px-2 py-1 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded min-w-[44px] min-h-[32px] flex items-center justify-center"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -87,7 +92,7 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
|
||||
e.stopPropagation();
|
||||
handleDelete(site.id, site.name);
|
||||
}}
|
||||
className="px-2 py-1 text-xs text-red-600 hover:bg-red-50 rounded"
|
||||
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded min-w-[32px] min-h-[32px] flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user