138 lines
5.2 KiB
TypeScript
138 lines
5.2 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { Globe2, RefreshCw, Upload, BadgeCheck, Mail, FileDown } from 'lucide-react';
|
|
|
|
export type QSOMenuState = { x: number; y: number; ids: number[] } | null;
|
|
|
|
type Props = {
|
|
menu: QSOMenuState;
|
|
onClose: () => void;
|
|
onUpdateFromCty: (ids: number[]) => void;
|
|
onUpdateFromQRZ: (ids: number[]) => void;
|
|
onUpdateFromClublog?: (ids: number[]) => void;
|
|
onSendTo?: (service: string, ids: number[]) => void;
|
|
onSendRecording?: (ids: number[]) => void;
|
|
onExportSelected?: (ids: number[]) => void;
|
|
onExportFiltered?: () => void;
|
|
};
|
|
|
|
const UPLOAD_TARGETS: { service: string; label: string }[] = [
|
|
{ service: 'qrz', label: 'Send to QRZ.com' },
|
|
{ service: 'clublog', label: 'Send to Club Log' },
|
|
{ service: 'lotw', label: 'Send to LoTW' },
|
|
];
|
|
|
|
// Lightweight right-click menu for the QSO grids. AG Grid's native context
|
|
// menu is an Enterprise feature, so this is a plain floating menu driven by
|
|
// onCellContextMenu. Closes on any outside click, scroll or Escape.
|
|
export function QSOContextMenu({ menu, onClose, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onExportSelected, onExportFiltered }: Props) {
|
|
useEffect(() => {
|
|
if (!menu) return;
|
|
const close = () => onClose();
|
|
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
window.addEventListener('mousedown', close);
|
|
window.addEventListener('scroll', close, true);
|
|
window.addEventListener('resize', close);
|
|
window.addEventListener('keydown', onKey);
|
|
return () => {
|
|
window.removeEventListener('mousedown', close);
|
|
window.removeEventListener('scroll', close, true);
|
|
window.removeEventListener('resize', close);
|
|
window.removeEventListener('keydown', onKey);
|
|
};
|
|
}, [menu, onClose]);
|
|
|
|
if (!menu) return null;
|
|
const n = menu.ids.length;
|
|
// Keep the menu on-screen near the cursor.
|
|
const x = Math.min(menu.x, window.innerWidth - 248);
|
|
const y = Math.min(menu.y, window.innerHeight - (onSendTo ? 230 : 110));
|
|
|
|
return (
|
|
<div
|
|
className="fixed z-[200] min-w-[240px] rounded-md border border-border bg-popover shadow-lg py-1 text-sm"
|
|
style={{ left: x, top: y }}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="px-3 py-1 text-[11px] uppercase tracking-wider text-muted-foreground">
|
|
{n} QSO{n > 1 ? 's' : ''} selected
|
|
</div>
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onUpdateFromCty(menu.ids); onClose(); }}
|
|
>
|
|
<Globe2 className="size-4 text-primary" />
|
|
<span>Fix country & zones from cty.dat</span>
|
|
</button>
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onUpdateFromQRZ(menu.ids); onClose(); }}
|
|
>
|
|
<RefreshCw className="size-4 text-sky-600" />
|
|
<span>Update from QRZ.com</span>
|
|
</button>
|
|
{onUpdateFromClublog && (
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onUpdateFromClublog(menu.ids); onClose(); }}
|
|
>
|
|
<BadgeCheck className="size-4 text-violet-600" />
|
|
<span>Update from ClubLog (exceptions)</span>
|
|
</button>
|
|
)}
|
|
|
|
{onSendRecording && (
|
|
<>
|
|
<div className="my-1 border-t border-border" />
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onSendRecording(menu.ids); onClose(); }}
|
|
>
|
|
<Mail className="size-4 text-rose-600" />
|
|
<span>Send recording by e-mail</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{(onExportSelected || onExportFiltered) && (
|
|
<>
|
|
<div className="my-1 border-t border-border" />
|
|
{onExportSelected && (
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onExportSelected(menu.ids); onClose(); }}
|
|
>
|
|
<FileDown className="size-4 text-sky-600" />
|
|
<span>Export selected to ADIF ({n})</span>
|
|
</button>
|
|
)}
|
|
{onExportFiltered && (
|
|
<button
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onExportFiltered(); onClose(); }}
|
|
>
|
|
<FileDown className="size-4 text-violet-600" />
|
|
<span>Export filtered view to ADIF (no limit)</span>
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{onSendTo && (
|
|
<>
|
|
<div className="my-1 border-t border-border" />
|
|
{UPLOAD_TARGETS.map((t) => (
|
|
<button
|
|
key={t.service}
|
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
|
|
onClick={() => { onSendTo(t.service, menu.ids); onClose(); }}
|
|
>
|
|
<Upload className="size-4 text-emerald-600" />
|
|
<span>{t.label}</span>
|
|
</button>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|