Files
rfcp/frontend/src/components/panels/SiteList.tsx

157 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Site } from '@/types/index.ts';
import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import Button from '@/components/ui/Button.tsx';
import BatchEdit from './BatchEdit.tsx';
interface SiteListProps {
onEditSite: (site: Site) => void;
onAddSite: () => void;
}
export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
const sites = useSitesStore((s) => s.sites);
const deleteSite = useSitesStore((s) => s.deleteSite);
const selectSite = useSitesStore((s) => s.selectSite);
const selectedSiteId = useSitesStore((s) => s.selectedSiteId);
const isPlacingMode = useSitesStore((s) => s.isPlacingMode);
const togglePlacingMode = useSitesStore((s) => s.togglePlacingMode);
const selectedSiteIds = useSitesStore((s) => s.selectedSiteIds);
const toggleSiteSelection = useSitesStore((s) => s.toggleSiteSelection);
const selectAllSites = useSitesStore((s) => s.selectAllSites);
const clearSelection = useSitesStore((s) => s.clearSelection);
const addToast = useToastStore((s) => s.addToast);
const handleDelete = async (id: string, name: string) => {
await deleteSite(id);
addToast(`"${name}" deleted`, 'info');
};
const allSelected = sites.length > 0 && selectedSiteIds.length === sites.length;
return (
<div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm">
{/* Header */}
<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">
<Button
size="sm"
variant={isPlacingMode ? 'danger' : 'primary'}
onClick={togglePlacingMode}
>
{isPlacingMode ? 'Cancel' : '+ Place on Map'}
</Button>
<Button size="sm" variant="secondary" onClick={onAddSite}>
+ Manual
</Button>
</div>
</div>
{/* Select All row (only when sites exist) */}
{sites.length > 0 && (
<div className="px-4 py-1.5 border-b border-gray-100 dark:border-dark-border flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer text-xs text-gray-500 dark:text-dark-muted">
<input
type="checkbox"
checked={allSelected}
onChange={() => (allSelected ? clearSelection() : selectAllSites())}
className="w-3.5 h-3.5 rounded border-gray-300 dark:border-dark-border accent-blue-600"
/>
{allSelected ? 'Deselect All' : 'Select All'}
</label>
{selectedSiteIds.length > 0 && (
<span className="text-xs text-blue-600 dark:text-blue-400">
{selectedSiteIds.length} selected
</span>
)}
</div>
)}
{/* Batch edit panel (appears when sites are selected) */}
{selectedSiteIds.length > 0 && (
<div className="px-3 pt-3">
<BatchEdit />
</div>
)}
{/* Sites list */}
{sites.length === 0 ? (
<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 dark:divide-dark-border max-h-60 overflow-y-auto">
{sites.map((site) => {
const isBatchSelected = selectedSiteIds.includes(site.id);
return (
<div
key={site.id}
className={`px-4 py-2.5 flex items-center gap-2.5 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' : ''}
${isBatchSelected && selectedSiteId !== site.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`}
onClick={() => selectSite(site.id)}
>
{/* Batch checkbox */}
<input
type="checkbox"
checked={isBatchSelected}
onChange={() => toggleSiteSelection(site.id)}
onClick={(e) => e.stopPropagation()}
className="w-3.5 h-3.5 rounded border-gray-300 dark:border-dark-border accent-blue-600 flex-shrink-0"
/>
{/* Color indicator */}
<div
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 dark:text-dark-text truncate">
{site.name}
</div>
<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>
{/* Actions */}
<div className="flex gap-1 flex-shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
onEditSite(site);
}}
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>
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(site.id, site.name);
}}
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>
</div>
</div>
);
})}
</div>
)}
</div>
);
}