diff --git a/package-lock.json b/package-lock.json index 3660e00..57f4438 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "ecpair": "^3.0.0", "embla-carousel-react": "^8.3.0", "hls.js": "^1.6.15", + "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.3", "input-otp": "^1.2.4", @@ -5875,6 +5876,15 @@ "node": ">=18" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/html5-qrcode": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", diff --git a/package.json b/package.json index 4da5a1d..db63369 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "ecpair": "^3.0.0", "embla-carousel-react": "^8.3.0", "hls.js": "^1.6.15", + "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.3", "input-otp": "^1.2.4", diff --git a/src/index.css b/src/index.css index c94b163..b74adc3 100644 --- a/src/index.css +++ b/src/index.css @@ -517,64 +517,17 @@ html, body { .card-theme-sunset .card-muted { color: rgba(255,208,160,0.65) !important; } .card-theme-sunset .card-bg-chip { background: rgba(255,80,0,0.2) !important; border: 1px solid rgba(255,80,0,0.5) !important; } -/* ── Print-only card: hidden on screen, shown only when printing ── */ -.print-only-card { - display: none !important; - position: fixed; - top: 0; - left: 0; - z-index: -1; -} - -/* ── Print styles ── */ +/* ── Print: the card is rendered to PNG via html2canvas before printing. + The @media print block below is only a safety fallback for direct Ctrl+P. + It will never look as good as the canvas approach. ── */ @media print { - /* Step 1: hide the entire page */ - html, body { - background: white !important; - } - - body > * { - display: none !important; - } - - /* Step 2: show only the print-only-card container */ - .print-only-card { + @page { size: 85.6mm 54mm; margin: 0; } + body > * { display: none !important; } + .rider-card-print-fallback { display: block !important; position: fixed !important; - top: 0 !important; - left: 0 !important; + top: 0; left: 0; width: 85.6mm !important; height: 54mm !important; - margin: 0 !important; - padding: 0 !important; - z-index: 99999 !important; - overflow: hidden !important; - } - - /* Step 3: size the card to exact credit-card dimensions */ - .print-only-card .rider-card { - width: 85.6mm !important; - height: 54mm !important; - border-radius: 4mm !important; - page-break-inside: avoid; - box-shadow: none !important; - overflow: hidden !important; - } - - /* Reset page margins to zero */ - @page { - size: 85.6mm 54mm; - margin: 0; - } - - /* Kill holographic overlay on print */ - .rider-card-holo::after { - display: none !important; - } - - /* Kill animations */ - *, *::before, *::after { - animation: none !important; - transition: none !important; } } diff --git a/src/pages/RiderCardPage.tsx b/src/pages/RiderCardPage.tsx index 37827fd..ca93777 100644 --- a/src/pages/RiderCardPage.tsx +++ b/src/pages/RiderCardPage.tsx @@ -13,9 +13,10 @@ * Print-ready: credit-card size (85.6×54mm), @media print isolates card */ -import { useState, useRef, useCallback, useEffect } from 'react'; +import { useState, useRef, useCallback } from 'react'; import { useSeoMeta } from '@unhead/react'; import { nip19 } from 'nostr-tools'; +import html2canvas from 'html2canvas'; import { Layout } from '@/components/Layout'; import { LoginArea } from '@/components/auth/LoginArea'; @@ -42,9 +43,9 @@ import { cn } from '@/lib/utils'; import type { NostrEvent } from '@nostrify/nostrify'; import { - Printer, Copy, Palette, Check, Building2, - User, Shield, Award, Plus, Search, Zap, - ChevronDown, ChevronUp, Info, + Printer, Download, Copy, Palette, Check, Building2, + User, Shield, Award, Zap, + ChevronDown, ChevronUp, Info, Loader2, } from 'lucide-react'; // ─── Nostr kinds ────────────────────────────────────────────────────────────── @@ -494,9 +495,9 @@ function AwardBadgeDialog({ open, onClose, issuerPubkey }: AwardBadgeDialogProps
- 🔐 On-chain proof.{' '} - This badge is cryptographically signed by your Nostr key. The rider can - display it with your name as witness — and anyone can verify it. + 🔐 Signed proof.{' '} + This badge is a signed message on the Nostr network — cryptographically tied to your key. + The rider can show it with your name as witness, and anyone can verify it's real.
+