qsl designer
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// 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);
|
||||
} 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-rose-600" /> Send eQSL by e-mail
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 px-6 py-5">
|
||||
{error && (
|
||||
<div className="rounded border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">{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-emerald-600">
|
||||
<CheckCircle2 className="size-4" /> eQSL 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user