148 lines
6.1 KiB
TypeScript
148 lines
6.1 KiB
TypeScript
import React, { useMemo, useRef } from 'react';
|
|
import { QRCodeSVG, QRCodeCanvas } from 'qrcode.react';
|
|
import { MapPin, ScanLine, AlertTriangle, Download } from 'lucide-react';
|
|
import { Button } from '@astryxdesign/core/Button';
|
|
import { buildStoreQrPayload } from '../services/fiestaApi';
|
|
|
|
interface StoreQRViewProps {
|
|
/** Merchant tenant the QR is scoped to. */
|
|
tenantId: number;
|
|
/** Resolved outlet id. When absent/0, the QR can't be scoped to a store. */
|
|
locationid?: number;
|
|
/** Outlet display name (shown on the printable card). */
|
|
storeName: string;
|
|
/** Zone label, e.g. "R.S. Puram, Coimbatore". */
|
|
storeZone?: string;
|
|
/** Full street address, when available. */
|
|
storeAddress?: string;
|
|
}
|
|
|
|
const QR_FG = '#000000';
|
|
const QR_BG = '#ffffff';
|
|
const QR_MARGIN = 4;
|
|
const QR_LEVEL = 'Q' as const;
|
|
|
|
export default function StoreQRView({
|
|
tenantId,
|
|
locationid,
|
|
storeName,
|
|
storeZone,
|
|
storeAddress,
|
|
}: StoreQRViewProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
const payload = useMemo(
|
|
() => (locationid ? buildStoreQrPayload({ tenantid: tenantId, locationid }) : ''),
|
|
[tenantId, locationid],
|
|
);
|
|
|
|
const slug = useMemo(() => {
|
|
return storeName
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '') || 'store';
|
|
}, [storeName]);
|
|
|
|
const downloadPng = () => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const a = document.createElement('a');
|
|
a.href = canvas.toDataURL('image/png');
|
|
a.download = `${slug}-qr.png`;
|
|
a.click();
|
|
};
|
|
|
|
if (!locationid) {
|
|
return (
|
|
<div className="max-w-xl mx-auto animate-in fade-in duration-300">
|
|
<div className="bg-white border border-slate-200/70 p-10 text-center shadow-[0_10px_40px_rgba(0,0,0,0.06)]">
|
|
<div className="mx-auto h-16 w-16 rounded-2xl bg-amber-50 text-amber-600 ring-1 ring-amber-100 flex items-center justify-center mb-6">
|
|
<AlertTriangle size={30} />
|
|
</div>
|
|
<h1 className="text-xl font-bold text-slate-900 tracking-tight mb-2">No store to encode</h1>
|
|
<p className="text-sm text-slate-500 leading-relaxed">
|
|
This QR opens a specific outlet's storefront, but no outlet is resolved yet. Once a store
|
|
location is assigned, its scannable code appears here.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto flex flex-col items-center" style={{ width: '100%', maxWidth: '380px', minWidth: '280px' }}>
|
|
|
|
{/* Premium Table Tent Mockup Card */}
|
|
<div className="bg-white border border-slate-200/70 shadow-[0_20px_50px_rgba(88,28,135,0.08)] overflow-hidden relative group transition-all duration-355 hover:shadow-[0_25px_60px_rgba(88,28,135,0.14)]" style={{ width: '100%' }}>
|
|
|
|
{/* Ambient Background Glow inside card */}
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/5 rounded-full blur-xl -mr-6 -mt-6 pointer-events-none group-hover:bg-purple-500/10 transition-colors" />
|
|
|
|
{/* Gradient Header banner */}
|
|
<div className="relative bg-gradient-to-br from-[#662582] via-[#6b21a8] to-purple-950 px-6 py-8 text-center text-white overflow-hidden">
|
|
<div className="absolute -top-12 -right-10 w-36 h-36 bg-purple-400/25 rounded-full blur-2xl pointer-events-none" />
|
|
<span className="relative inline-flex items-center gap-1.5 text-[9px] font-extrabold uppercase tracking-[0.16em] text-purple-100 bg-white/10 border border-white/20 rounded-full px-3.5 py-1.5 backdrop-blur-sm shadow-inner-sm">
|
|
<ScanLine size={11} className="text-purple-200 animate-pulse" /> Scan & Shop
|
|
</span>
|
|
<h3 className="relative font-black text-lg mt-4 leading-snug tracking-tight text-white">{storeName}</h3>
|
|
{(storeZone || storeAddress) && (
|
|
<p className="relative text-[10px] text-purple-200 mt-2 flex items-center justify-center gap-1 font-medium">
|
|
<MapPin size={11} className="shrink-0 text-purple-300" />
|
|
<span className="truncate max-w-[200px]">{storeZone || storeAddress}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* QR Code Frame with Overlap Layout */}
|
|
<div className="px-6 pb-8 pt-6 flex flex-col items-center relative z-10">
|
|
<div className="-mt-12 p-4 bg-white rounded-2xl border border-slate-100 shadow-[0_12px_36px_rgba(15,23,42,0.12)] hover:scale-[1.02] transition-transform duration-300">
|
|
<QRCodeSVG
|
|
data-qr-svg="true"
|
|
value={payload}
|
|
size={180}
|
|
level={QR_LEVEL}
|
|
marginSize={QR_MARGIN}
|
|
fgColor={QR_FG}
|
|
bgColor={QR_BG}
|
|
title={`${storeName} storefront QR`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Counter instructions info block */}
|
|
<div className="mt-5 text-center space-y-1">
|
|
<p className="text-[11px] font-extrabold text-slate-800 uppercase tracking-wider">Storefront QR Code</p>
|
|
<p className="text-[10px] text-slate-454 max-w-[220px] leading-relaxed mx-auto font-medium">
|
|
Customers scan this using the consumer app to shop this branch.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Download Action Button */}
|
|
<div className="w-full mt-6">
|
|
<Button
|
|
label="Download QR Code"
|
|
icon={<Download size={14} />}
|
|
onClick={downloadPng}
|
|
style={{ height: 'auto' }}
|
|
className="w-full flex items-center justify-center gap-2 py-3 px-5 bg-gradient-to-r from-[#662582] to-indigo-650 hover:from-purple-800 hover:to-indigo-700 text-white rounded-xl text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 transition-all shadow-md border-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Hidden high-res canvas — source for the PNG download */}
|
|
<div className="sr-only" aria-hidden="true">
|
|
<QRCodeCanvas
|
|
ref={canvasRef}
|
|
value={payload}
|
|
size={1024}
|
|
level={QR_LEVEL}
|
|
marginSize={QR_MARGIN}
|
|
fgColor={QR_FG}
|
|
bgColor={QR_BG}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|