diff --git a/app/globals.css b/app/globals.css index 2ad0345..c21895f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -244,6 +244,57 @@ select.ui-control { background: rgb(var(--color-panel) / 1); } +.method-qr-details { + position: relative; +} + +.method-qr-details > summary { + list-style: none; +} + +.method-qr-details > summary::-webkit-details-marker { + display: none; +} + +.method-qr-panel { + position: absolute; + right: 0; + top: calc(100% + 8px); + width: min(22rem, 86vw); + border: 1px solid rgb(var(--color-border) / 0.9); + border-radius: 8px; + background: rgb(var(--color-panel) / 0.98); + padding: 12px; + display: grid; + gap: 10px; + z-index: 70; +} + +.method-qr-title { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.method-qr-image { + width: 220px; + height: 220px; + margin: 0 auto; + background: #fff; + border-radius: 6px; + padding: 6px; +} + +.method-qr-payload { + font-size: 12px; + line-height: 1.5; + color: rgb(var(--color-muted)); + word-break: break-all; + max-height: 110px; + overflow-y: auto; +} + .dashboard-action { min-height: 38px; padding: 8px 12px; diff --git a/app/u/[username]/page.tsx b/app/u/[username]/page.tsx index 077b3e9..2db5e01 100644 --- a/app/u/[username]/page.tsx +++ b/app/u/[username]/page.tsx @@ -1,7 +1,9 @@ import { notFound } from "next/navigation"; import type { CSSProperties } from "react"; +import QRCode from "qrcode"; import { db } from "@/lib/db"; -import { THEME_TOKENS } from "@/lib/constants"; +import { SUPPORTS_QR, THEME_TOKENS } from "@/lib/constants"; +import { buildPaymentUri } from "@/lib/payment-uri"; import { ProfileHeader } from "@/components/public/profile-header"; import { PaymentMethodCard } from "@/components/public/payment-method-card"; import { SocialLinksList } from "@/components/public/social-links-list"; @@ -54,6 +56,23 @@ export default async function PublicProfilePage({ params }: PublicProfilePagePro notFound(); } + const methodsWithQr = await Promise.all( + profile.paymentMethods.map(async (method) => { + if (!SUPPORTS_QR.has(method.type)) { + return { ...method, qrPayload: null, qrDataUrl: null }; + } + + const qrPayload = buildPaymentUri(method.type, method.value, method.network); + + try { + const qrDataUrl = await QRCode.toDataURL(qrPayload, { margin: 1, width: 280 }); + return { ...method, qrPayload, qrDataUrl }; + } catch { + return { ...method, qrPayload, qrDataUrl: null }; + } + }) + ); + const tokens = THEME_TOKENS[profile.themeId] ?? THEME_TOKENS["terminal-dark"]; return ( @@ -86,7 +105,7 @@ export default async function PublicProfilePage({ params }: PublicProfilePagePro

) : (
- {profile.paymentMethods.map((method) => ( + {methodsWithQr.map((method) => ( ))}
diff --git a/components/public/payment-method-card.tsx b/components/public/payment-method-card.tsx index f7b12b6..cd86150 100644 --- a/components/public/payment-method-card.tsx +++ b/components/public/payment-method-card.tsx @@ -1,15 +1,17 @@ "use client"; +/* eslint-disable @next/next/no-img-element */ import { PaymentMethod, PaymentMethodType } from "@prisma/client"; -import { PAYMENT_METHOD_LABELS, SUPPORTS_QR } from "@/lib/constants"; -import { buildPaymentUri } from "@/lib/payment-uri"; +import { PAYMENT_METHOD_LABELS } from "@/lib/constants"; import { CopyButton } from "@/components/public/copy-button"; -import { QrModal } from "@/components/public/qr-modal"; type PublicPaymentMethod = Pick< PaymentMethod, "id" | "type" | "label" | "value" | "network" | "description" | "isVisible" ->; +> & { + qrPayload: string | null; + qrDataUrl: string | null; +}; type PaymentMethodCardProps = { method: PublicPaymentMethod; @@ -26,8 +28,7 @@ function isHttpUrl(value: string) { export function PaymentMethodCard({ method }: PaymentMethodCardProps) { const typeLabel = PAYMENT_METHOD_LABELS[method.type as PaymentMethodType]; - const qrPayload = buildPaymentUri(method.type, method.value, method.network); - const linkTarget = isHttpUrl(qrPayload) ? qrPayload : null; + const linkTarget = method.qrPayload && isHttpUrl(method.qrPayload) ? method.qrPayload : null; return (
@@ -44,7 +45,20 @@ export function PaymentMethodCard({ method }: PaymentMethodCardProps) {
- {SUPPORTS_QR.has(method.type) ? : null} + {method.qrPayload ? ( +
+ QR +
+

{method.label} QR

+ {method.qrDataUrl ? ( + {`${method.label} + ) : ( +

Could not generate QR code for this value.

+ )} +

{method.qrPayload}

+
+
+ ) : null} {linkTarget ? ( Open diff --git a/components/ui/modal.tsx b/components/ui/modal.tsx index 45b5eef..8e06e3c 100644 --- a/components/ui/modal.tsx +++ b/components/ui/modal.tsx @@ -37,7 +37,7 @@ export function Modal({ open, title, onClose, children }: ModalProps) { return createPortal(