fix: remove orphaned canvas code from RiderCardPage causing ctx/Printer is not defined crash
The drawCardToCanvas function had an old version of the card rendering logic that leaked outside the function body (lines 505-731 were orphaned code). This code referenced local variables (ctx, p, data, qrX, qrSize, PAD, etc.) that only existed inside the function, causing ReferenceError at module load time and crashing the entire app with a blank screen. Also added missing lucide-react imports (Printer, Loader2, Share2, Download, Check, Copy, Award, Zap, Info, Palette, Shield, ChevronUp, ChevronDown) that were used in the JSX but not imported, causing the secondary Printer is not defined ReferenceError. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
125bce3637
commit
ebbf2d6326
8 changed files with 50 additions and 243 deletions
|
|
@ -6,7 +6,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
|||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
import { toast } from '@/hooks/useToast';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
|
||||
export function SecuritySettings() {
|
||||
const { user } = useCurrentUser();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useNWC } from '@/hooks/useNWCContext';
|
|||
import { getInvoiceFromLightningAddress } from '@/lib/lightningAddress';
|
||||
import { LISTING_FEE_SATS, PLATFORM_LIGHTNING_ADDRESS } from '@/hooks/useListingRestrictions';
|
||||
import { Zap, Loader2, CheckCircle, Copy, ExternalLink, ShieldAlert, TrendingUp, MapPin } from 'lucide-react';
|
||||
import QRCode from 'qrcode';
|
||||
// qrcode imported lazily in async function
|
||||
|
||||
interface SpamFeePaymentProps {
|
||||
feeRecipient: string;
|
||||
|
|
@ -52,6 +52,7 @@ export function SpamFeePayment({
|
|||
setInvoice(bolt11);
|
||||
|
||||
// Generate QR code
|
||||
const { default: QRCode } = await import('qrcode');
|
||||
const qr = await QRCode.toDataURL(bolt11.toUpperCase(), {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { useCashu } from '@/hooks/wallet/useCashu';
|
|||
import { useLightningOperations } from '@/hooks/wallet/useLightningOperations';
|
||||
import { WalletModal } from '@/components/WalletModal';
|
||||
import type { Event } from 'nostr-tools';
|
||||
import QRCode from 'qrcode';
|
||||
// qrcode imported lazily in async function to avoid canvas crash at module load
|
||||
import type { WebLNProvider } from "@webbtc/webln-types";
|
||||
|
||||
interface ZapDialogProps {
|
||||
|
|
@ -443,6 +443,7 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
|
|||
}
|
||||
|
||||
try {
|
||||
const { default: QRCode } = await import('qrcode');
|
||||
const url = await QRCode.toDataURL(invoice.toUpperCase(), {
|
||||
width: 512,
|
||||
margin: 2,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
mnemonicToWords,
|
||||
wordsToMnemonic
|
||||
} from '@/lib/nip06';
|
||||
import QRCode from 'qrcode';
|
||||
// qrcode imported lazily in async function
|
||||
|
||||
interface MnemonicSignupDialogProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -95,6 +95,7 @@ const MnemonicSignupDialog: React.FC<MnemonicSignupDialogProps> = ({
|
|||
// Generate QR code
|
||||
const handleQR = async () => {
|
||||
try {
|
||||
const { default: QRCode } = await import('qrcode');
|
||||
const qrDataUrl = await QRCode.toDataURL(mnemonic, {
|
||||
width: 300,
|
||||
margin: 2,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import QRCode from 'qrcode';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface QRCodeCanvasProps {
|
||||
value: string;
|
||||
|
|
@ -14,11 +13,21 @@ export function QRCodeCanvas({ value, size = 256, level = 'M', className }: QRCo
|
|||
useEffect(() => {
|
||||
if (!canvasRef.current || !value || value.length < 4) return;
|
||||
const el = canvasRef.current;
|
||||
try {
|
||||
QRCode.toCanvas(el, value, { width: size, margin: 1, errorCorrectionLevel: level });
|
||||
} catch {
|
||||
// silently skip on invalid input
|
||||
}
|
||||
|
||||
// Lazy import to avoid crashing at module load in restricted environments
|
||||
import('qrcode').then((mod) => {
|
||||
try {
|
||||
mod.default.toCanvas(el, value, {
|
||||
width: size,
|
||||
margin: 1,
|
||||
errorCorrectionLevel: level,
|
||||
});
|
||||
} catch {
|
||||
// silently skip
|
||||
}
|
||||
}).catch(() => {
|
||||
// qrcode unavailable in this environment
|
||||
});
|
||||
}, [value, size, level]);
|
||||
|
||||
return <canvas ref={canvasRef} width={size} height={size} className={className} />;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useCallback, useRef } from 'react';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
// Html5Qrcode imported lazily inside startScanning to avoid canvas crash at module load
|
||||
type Html5QrcodeType = import('html5-qrcode').Html5Qrcode;
|
||||
|
||||
export type QRScanResult =
|
||||
| { type: 'lightning_invoice'; value: string }
|
||||
|
|
@ -49,7 +50,7 @@ export function classifyQRCode(decodedText: string): QRScanResult {
|
|||
export function useQRCodeScanner() {
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const scannerRef = useRef<Html5Qrcode | null>(null);
|
||||
const scannerRef = useRef<Html5QrcodeType | null>(null);
|
||||
const isStartingRef = useRef(false);
|
||||
const shouldCancelRef = useRef(false);
|
||||
|
||||
|
|
@ -61,6 +62,9 @@ export function useQRCodeScanner() {
|
|||
shouldCancelRef.current = false;
|
||||
setError(null);
|
||||
|
||||
// Lazy load to avoid canvas crash at module load time
|
||||
const { Html5Qrcode } = await import('html5-qrcode');
|
||||
|
||||
// Find available cameras
|
||||
const devices = await Html5Qrcode.getCameras();
|
||||
if (devices.length === 0) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import QRCode from 'qrcode';
|
||||
|
||||
/**
|
||||
* Generate a QR code data URL from text
|
||||
*
|
||||
* qrcode imported lazily to avoid canvas crash at module load
|
||||
* @param text - Text to encode in QR code
|
||||
* @returns Promise resolving to data URL
|
||||
*/
|
||||
export async function generateQRCode(text: string): Promise<string> {
|
||||
try {
|
||||
const { default: QRCode } = await import('qrcode');
|
||||
const dataUrl = await QRCode.toDataURL(text, {
|
||||
width: 300,
|
||||
margin: 2,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import QRCode from 'qrcode';
|
||||
// QRCode is imported lazily inside drawCardToCanvas() to avoid canvas API
|
||||
// access at module load time (crashes the preview environment)
|
||||
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
|
|
@ -41,6 +42,21 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import { genUserName } from '@/lib/genUserName';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import {
|
||||
Printer,
|
||||
Loader2,
|
||||
Share2,
|
||||
Download,
|
||||
Check,
|
||||
Copy,
|
||||
Award,
|
||||
Zap,
|
||||
Info,
|
||||
Palette,
|
||||
Shield,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
} from 'lucide-react';
|
||||
// ─── Card canvas renderer ─────────────────────────────────────────────────────
|
||||
// Draws the card entirely via Canvas 2D API — 100% reliable export,
|
||||
// no CSS capture, no scaling issues, no background-stripping.
|
||||
|
|
@ -192,6 +208,8 @@ async function drawCardToCanvas(data: CanvasCardData): Promise<string> {
|
|||
const qrY = 28;
|
||||
let qrImg: HTMLImageElement | null = null;
|
||||
try {
|
||||
// Dynamic import — avoids canvas API access at module load time
|
||||
const QRCode = (await import('qrcode')).default;
|
||||
const qrDataUrl = await QRCode.toDataURL(qrUrl, {
|
||||
width: qrSize * 2,
|
||||
margin: 2,
|
||||
|
|
@ -499,232 +517,6 @@ async function drawCardToCanvas(data: CanvasCardData): Promise<string> {
|
|||
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '15px system-ui, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('SCAN TO VERIFY', qrX + qrSize / 2, qrY + qrSize + 22);
|
||||
ctx.textAlign = 'left';
|
||||
|
||||
if (data.type === 'rider') {
|
||||
// ── Header: brand left, npub right ──────────────────────────────────────
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.font = 'bold 30px system-ui, sans-serif';
|
||||
ctx.fillText('🛹 SKATESPOTS', PAD, 54);
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '17px system-ui, sans-serif';
|
||||
ctx.fillText('SKATER LICENSE', PAD, 76);
|
||||
|
||||
// Full npub split into two lines to fit (left of QR)
|
||||
const npubMaxW = qrX - PAD - 20;
|
||||
const npub1 = data.npub.slice(0, 32);
|
||||
const npub2 = data.npub.slice(32);
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '14px monospace';
|
||||
ctx.fillText(npub1, PAD, 100);
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.fillText(npub2, PAD, 118);
|
||||
|
||||
// ── Avatar (square, left column) ────────────────────────────────────────
|
||||
const avatarSize = 180;
|
||||
const avatarX = PAD;
|
||||
const avatarY = 135;
|
||||
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
|
||||
ctx.fillStyle = p.chipBg;
|
||||
ctx.fill();
|
||||
if (data.avatarUrl) {
|
||||
const img = await loadImage(data.avatarUrl);
|
||||
if (img) {
|
||||
ctx.save();
|
||||
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
|
||||
ctx.clip();
|
||||
ctx.drawImage(img, avatarX, avatarY, avatarSize, avatarSize);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Identity column (right of avatar, left of QR) ───────────────────────
|
||||
const idX = PAD + avatarSize + 28;
|
||||
const idMaxW = qrX - idX - 16;
|
||||
let cy = 155; // current Y cursor
|
||||
|
||||
// Name
|
||||
ctx.fillStyle = p.text;
|
||||
ctx.font = 'bold 46px system-ui, sans-serif';
|
||||
fillTextFit(ctx, data.name.toUpperCase(), idX, cy, idMaxW);
|
||||
cy += 52;
|
||||
|
||||
// nip05
|
||||
if (data.nip05) {
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.font = '19px system-ui, sans-serif';
|
||||
fillTextFit(ctx, `@${data.nip05.split('@')[0]}`, idX, cy, idMaxW);
|
||||
cy += 26;
|
||||
}
|
||||
|
||||
// Style chip
|
||||
if (data.style) {
|
||||
ctx.font = 'bold 17px system-ui, sans-serif';
|
||||
const chipTxt = data.style.toUpperCase();
|
||||
const chipW = ctx.measureText(chipTxt).width + 22;
|
||||
roundRect(ctx, idX, cy, chipW, 28, 6);
|
||||
ctx.fillStyle = p.chipBg;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.fillText(chipTxt, idX + 11, cy + 19);
|
||||
cy += 38;
|
||||
}
|
||||
|
||||
// ── SHOP section (explicit labeled) ─────────────────────────────────────
|
||||
if (data.ridesFor) {
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '13px system-ui, sans-serif';
|
||||
ctx.fillText('SKATE SHOP', idX, cy);
|
||||
cy += 4;
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.font = 'bold 19px system-ui, sans-serif';
|
||||
fillTextFit(ctx, data.ridesFor, idX, cy + 18, idMaxW);
|
||||
cy += 32;
|
||||
}
|
||||
|
||||
// ── SPONSORS section (explicit labeled) ─────────────────────────────────
|
||||
if (data.sponsors) {
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '13px system-ui, sans-serif';
|
||||
ctx.fillText('SPONSORS', idX, cy);
|
||||
cy += 4;
|
||||
ctx.fillStyle = p.text;
|
||||
ctx.font = '18px system-ui, sans-serif';
|
||||
fillTextFit(ctx, data.sponsors, idX, cy + 18, idMaxW);
|
||||
cy += 32;
|
||||
}
|
||||
|
||||
// ── Stats row ────────────────────────────────────────────────────────────
|
||||
const statItems = [
|
||||
data.showSpots ? { label: 'SPOTS', value: String(data.spotsAdded), color: p.accent } : null,
|
||||
data.showZaps ? { label: 'ZAPS ⚡', value: String(data.zapsReceived), color: p.accent2 } : null,
|
||||
data.showEvents ? { label: 'EVENTS', value: String(data.eventsCreated), color: '#ff6600' } : null,
|
||||
].filter(Boolean) as { label: string; value: string; color: string }[];
|
||||
|
||||
if (statItems.length > 0) {
|
||||
cy = Math.max(cy, 330);
|
||||
const statW = idMaxW / statItems.length;
|
||||
statItems.forEach((s, i) => {
|
||||
const sx = idX + i * statW;
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '13px system-ui, sans-serif';
|
||||
ctx.fillText(s.label, sx, cy);
|
||||
ctx.fillStyle = s.color;
|
||||
ctx.font = 'bold 28px system-ui, sans-serif';
|
||||
ctx.fillText(s.value, sx, cy + 30);
|
||||
});
|
||||
cy += 50;
|
||||
}
|
||||
|
||||
// ── Skill badges ─────────────────────────────────────────────────────────
|
||||
const skillsToShow = [
|
||||
...data.skills.slice(0, 5).map(id => {
|
||||
const s = SKILL_OPTIONS.find(o => o.id === id);
|
||||
return s ? { label: `${s.label}`, gold: false } : null;
|
||||
}).filter(Boolean),
|
||||
...data.receivedBadges.slice(0, 3).map(b => {
|
||||
const s = SKILL_OPTIONS.find(o => o.id === b.skill);
|
||||
return s ? { label: `★ ${s.label}`, gold: true } : null;
|
||||
}).filter(Boolean),
|
||||
] as { label: string; gold: boolean }[];
|
||||
|
||||
if (skillsToShow.length > 0) {
|
||||
cy = Math.max(cy, 390);
|
||||
ctx.font = 'bold 15px system-ui, sans-serif';
|
||||
let chipX = idX;
|
||||
skillsToShow.forEach(chip => {
|
||||
const chipW = ctx.measureText(chip.label).width + 18;
|
||||
if (chipX + chipW > qrX - 8) { chipX = idX; cy += 30; }
|
||||
roundRect(ctx, chipX, cy, chipW, 24, 5);
|
||||
ctx.fillStyle = chip.gold ? 'rgba(255,215,0,0.2)' : p.chipBg;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = chip.gold ? '#ffd700' : p.accent;
|
||||
ctx.fillText(chip.label, chipX + 9, cy + 17);
|
||||
chipX += chipW + 7;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Since year (under avatar) ────────────────────────────────────────────
|
||||
const sinceY = avatarY + avatarSize + 22;
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '14px system-ui, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`SINCE ${data.memberYear}`, PAD + avatarSize / 2, sinceY);
|
||||
ctx.textAlign = 'left';
|
||||
|
||||
} else {
|
||||
// ── PARK ACCESS card ─────────────────────────────────────────────────────
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.font = 'bold 30px system-ui, sans-serif';
|
||||
ctx.fillText(`🏟️ ${(data.issuerName || 'SKATEPARK').toUpperCase()}`, PAD, 54);
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '17px system-ui, sans-serif';
|
||||
ctx.fillText('ACCESS PASS', PAD, 76);
|
||||
|
||||
// Access level chip
|
||||
ctx.font = 'bold 19px system-ui, sans-serif';
|
||||
const alTxt = (data.accessLevel || 'FULL ACCESS').toUpperCase();
|
||||
const alW = ctx.measureText(alTxt).width + 28;
|
||||
roundRect(ctx, PAD, 90, alW, 32, 7);
|
||||
ctx.fillStyle = p.chipBg;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.fillText(alTxt, PAD + 14, 111);
|
||||
|
||||
// Avatar
|
||||
const avatarSize = 180;
|
||||
const avatarX = PAD;
|
||||
const avatarY = 140;
|
||||
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
|
||||
ctx.fillStyle = p.chipBg;
|
||||
ctx.fill();
|
||||
if (data.avatarUrl) {
|
||||
const img = await loadImage(data.avatarUrl);
|
||||
if (img) {
|
||||
ctx.save();
|
||||
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
|
||||
ctx.clip();
|
||||
ctx.drawImage(img, avatarX, avatarY, avatarSize, avatarSize);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
const idX = PAD + avatarSize + 28;
|
||||
const idMaxW = qrX - idX - 16;
|
||||
let cy = 170;
|
||||
|
||||
ctx.fillStyle = p.text;
|
||||
ctx.font = 'bold 44px system-ui, sans-serif';
|
||||
fillTextFit(ctx, (data.recipientName || 'MEMBER NAME').toUpperCase(), idX, cy, idMaxW);
|
||||
cy += 50;
|
||||
|
||||
// Full npub split into two lines
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '14px monospace';
|
||||
ctx.fillText(data.npub.slice(0, 32), idX, cy);
|
||||
cy += 20;
|
||||
ctx.fillText(data.npub.slice(32), idX, cy);
|
||||
cy += 28;
|
||||
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '16px system-ui, sans-serif';
|
||||
ctx.fillText('Issued by park owner · Nostr-signed', idX, cy);
|
||||
cy += 28;
|
||||
|
||||
if (data.validThru) {
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = '14px system-ui, sans-serif';
|
||||
ctx.fillText('VALID THRU', idX, cy);
|
||||
cy += 4;
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.font = 'bold 28px system-ui, sans-serif';
|
||||
ctx.fillText(data.validThru, idX, cy + 26);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Nostr kinds ──────────────────────────────────────────────────────────────
|
||||
export const PARK_ACCESS_CARD_KIND = 35679; // Park issues to rider
|
||||
|
|
|
|||
Loading…
Reference in a new issue