Files
OpsLog/frontend/src/components/qsl/SendEQSLModal.tsx
T

127 lines
4.5 KiB
TypeScript

// SendEQSLModal — per-QSO eQSL flow: render the default template with the
// QSO's data, show the exact card, send it as a JPEG e-mail attachment on
// explicit confirmation (never automatic). On success the backend stamps
// eqsl_sent and emits "qsl:sent" so the grid refreshes.
import { useEffect, useRef, useState } from 'react';
import { Loader2, Mail, CheckCircle2 } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
QSLDefaultTemplateID, RenderEQSL, SendEQSL,
} from '../../../wailsjs/go/main/App';
import type { RenderModel } from './qslTypes';
import { loadCardAssets, type CardAssets } from './qslAssets';
import { CardPreview } from './CardPreview';
import { rasterizeCard } from './rasterize';
interface Props {
open: boolean;
qsoId: number | null;
onClose: () => void;
onOpenDesigner: () => void;
}
export function SendEQSLModal({ open, qsoId, onClose, onOpenDesigner }: Props) {
const [model, setModel] = useState<RenderModel | null>(null);
const [assets, setAssets] = useState<CardAssets | null>(null);
const [templateId, setTemplateId] = useState(0);
const [busy, setBusy] = useState(false);
const [sent, setSent] = useState(false);
const [error, setError] = useState('');
const svgEl = useRef<SVGSVGElement | null>(null);
useEffect(() => {
if (!open || !qsoId) return;
setModel(null);
setAssets(null);
setSent(false);
setError('');
void (async () => {
try {
const tid = await QSLDefaultTemplateID();
setTemplateId(tid);
if (!tid) return; // no template yet — offer the designer below
const m = JSON.parse(await RenderEQSL(qsoId, tid)) as RenderModel;
setModel(m);
setAssets(await loadCardAssets(m.template, tid));
} catch (e) {
setError(String(e));
}
})();
}, [open, qsoId]);
async function send() {
if (!qsoId || !model || !svgEl.current) return;
setBusy(true);
setError('');
try {
const card = model.template.card;
const jpeg = await rasterizeCard(svgEl.current, card.w, card.h, 'image/jpeg');
await SendEQSL(qsoId, templateId, jpeg);
setSent(true);
// Auto-close shortly after the success tick (the grid refreshes on the
// qsl:sent event); the user doesn't need to dismiss it manually.
window.setTimeout(onClose, 1100);
} catch (e) {
setError(String(e));
} finally {
setBusy(false);
}
}
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="max-w-[820px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="size-5 text-danger" /> Send OpsLog QSL by e-mail
</DialogTitle>
</DialogHeader>
<div className="space-y-3 px-6 py-5">
{error && (
<div className="rounded border border-destructive bg-destructive-muted px-3 py-1.5 text-sm text-destructive-muted-foreground">{error}</div>
)}
{open && !templateId && !error && (
<div className="space-y-3 py-2 text-sm">
<p>No QSL template yet design one first (drop a few photos, pick a layout).</p>
<Button size="sm" onClick={() => { onClose(); onOpenDesigner(); }}>Open the designer</Button>
</div>
)}
{templateId > 0 && !model && !error && (
<div className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" /> Preparing the card
</div>
)}
{model && assets && (
<div className="rounded-md bg-slate-800/60 p-3">
<CardPreview model={model} assets={assets} width={740}
svgRef={(el) => { svgEl.current = el; }} />
</div>
)}
</div>
<DialogFooter>
{sent ? (
<div className="flex items-center gap-2 text-sm text-success">
<CheckCircle2 className="size-4" /> OpsLog QSL sent.
<Button variant="outline" size="sm" onClick={onClose}>Close</Button>
</div>
) : (
<>
<Button variant="outline" size="sm" onClick={onClose}>Cancel</Button>
<Button size="sm" disabled={!model || busy} onClick={send}>
{busy && <Loader2 className="mr-1 size-4 animate-spin" />} Send
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}