136 lines
6.4 KiB
TypeScript
136 lines
6.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Radio } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { cn } from '@/lib/utils';
|
|
import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n';
|
|
import { GetActiveProfile, SaveProfile, DownloadAllReferenceLists } from '../../wailsjs/go/main/App';
|
|
import type { profile as profileModels } from '../../wailsjs/go/models';
|
|
|
|
type Profile = Omit<profileModels.Profile, 'convertValues'>;
|
|
|
|
// FirstRunModal collects the mandatory station identity on the very first launch
|
|
// (no callsign configured yet). It writes straight into the active profile, so
|
|
// OpsLog has a valid station before any QSO is logged. Not dismissable.
|
|
export function FirstRunModal({ onDone }: { onDone: () => void }) {
|
|
const { t, lang, setLang } = useI18n();
|
|
const [p, setP] = useState<Profile | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [err, setErr] = useState('');
|
|
const [refsState, setRefsState] = useState<'idle' | 'loading' | 'done'>('idle');
|
|
const [refsMsg, setRefsMsg] = useState('');
|
|
|
|
async function downloadRefs() {
|
|
setRefsState('loading');
|
|
try {
|
|
const summary = await DownloadAllReferenceLists();
|
|
setRefsMsg(summary);
|
|
setRefsState('done');
|
|
} catch (e: any) {
|
|
setRefsMsg(String(e?.message ?? e));
|
|
setRefsState('idle');
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
GetActiveProfile().then((x) => setP(x as Profile)).catch(() => setP({} as Profile));
|
|
}, []);
|
|
|
|
const set = (patch: Partial<Profile>) => setP((s: Profile | null) => ({ ...(s as Profile), ...patch }));
|
|
|
|
const callsign = (p?.callsign ?? '').trim().toUpperCase();
|
|
const grid = (p?.my_grid ?? '').trim().toUpperCase();
|
|
const operator = (p?.operator ?? '').trim().toUpperCase();
|
|
const canSave = callsign.length >= 3 && grid.length >= 4;
|
|
|
|
async function save() {
|
|
if (!p || !canSave) return;
|
|
setSaving(true);
|
|
setErr('');
|
|
try {
|
|
await SaveProfile({
|
|
...p,
|
|
callsign,
|
|
my_grid: grid,
|
|
operator: operator || callsign,
|
|
owner_callsign: (p.owner_callsign ?? '').trim().toUpperCase() || callsign,
|
|
op_name: (p.op_name ?? '').trim(),
|
|
} as any);
|
|
onDone();
|
|
} catch (e: any) {
|
|
setErr(String(e?.message ?? e));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
|
<div className="w-full max-w-md rounded-xl border border-border bg-card shadow-2xl p-6 animate-in fade-in zoom-in-95">
|
|
{/* Language chooser — lives here (and not only in the localStorage-backed
|
|
first-launch flag gate) so a fresh setup always offers EN/FR, like the
|
|
station identity below. */}
|
|
<div className="flex justify-center mb-4">
|
|
<div className="inline-flex rounded-md border border-border overflow-hidden">
|
|
{([['en', FlagGB, 'English'], ['fr', FlagFR, 'Français']] as [Lang, typeof FlagGB, string][]).map(([code, Flag, label]) => (
|
|
<button key={code} type="button" onClick={() => setLang(code)}
|
|
className={cn('flex items-center gap-2 px-3 py-1.5 text-sm font-medium border-l border-border first:border-l-0 transition-colors',
|
|
lang === code ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
|
|
<Flag className="w-5 rounded-[2px] border border-border/30" />
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Radio className="size-5 text-primary" />
|
|
<h2 className="text-lg font-semibold">{t('frm.welcome')}</h2>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
{t('frm.intro')}
|
|
</p>
|
|
|
|
<div className="grid grid-cols-[120px_1fr] gap-x-3 gap-y-2.5 items-center">
|
|
<Label className="text-sm">{t('frm.callsign')} <span className="text-destructive">*</span></Label>
|
|
<Input autoFocus className="h-9 font-mono uppercase" placeholder="F4BPO" value={p?.callsign ?? ''} onChange={(e) => set({ callsign: e.target.value })} />
|
|
|
|
<Label className="text-sm">{t('frm.locator')} <span className="text-destructive">*</span></Label>
|
|
<Input className="h-9 font-mono uppercase" placeholder="JN03" value={p?.my_grid ?? ''} onChange={(e) => set({ my_grid: e.target.value })} />
|
|
|
|
<Label className="text-sm">{t('frm.operator')}</Label>
|
|
<Input className="h-9 font-mono uppercase" placeholder={t('frm.operatorPh')} value={p?.operator ?? ''} onChange={(e) => set({ operator: e.target.value })} />
|
|
|
|
<Label className="text-sm">{t('frm.owner')}</Label>
|
|
<Input className="h-9 font-mono uppercase" placeholder={t('frm.ownerPh')} value={p?.owner_callsign ?? ''} onChange={(e) => set({ owner_callsign: e.target.value })} />
|
|
|
|
<Label className="text-sm">{t('frm.name')}</Label>
|
|
<Input className="h-9" placeholder={t('frm.namePh')} value={p?.op_name ?? ''} onChange={(e) => set({ op_name: e.target.value })} />
|
|
</div>
|
|
|
|
{/* Optional: grab the award reference lists now (also in Tools later). */}
|
|
<div className="mt-4 rounded-lg border border-border bg-muted/30 p-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="text-xs">
|
|
<div className="font-medium">{t('frm.awardRefs')}</div>
|
|
<div className="text-muted-foreground">{t('frm.awardRefsHint')}</div>
|
|
</div>
|
|
<Button size="sm" variant="outline" disabled={refsState === 'loading'} onClick={downloadRefs}>
|
|
{refsState === 'loading' ? t('frm.downloading') : refsState === 'done' ? t('frm.reDownload') : t('frm.download')}
|
|
</Button>
|
|
</div>
|
|
{refsMsg && <div className={cn('text-[11px] mt-2', refsState === 'done' ? 'text-success' : 'text-destructive')}>{refsMsg}</div>}
|
|
</div>
|
|
|
|
{err && <div className="mt-3 text-xs text-destructive">{err}</div>}
|
|
|
|
<div className="mt-5 flex items-center justify-end gap-2">
|
|
{!canSave && <span className="text-[11px] text-muted-foreground mr-auto">{t('frm.required')}</span>}
|
|
<Button disabled={!canSave || saving} onClick={save}>{saving ? t('frm.saving') : t('frm.startLogging')}</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|