fix(profile): make QR action open inline panel on public profile
This commit is contained in:
@@ -244,6 +244,57 @@ select.ui-control {
|
|||||||
background: rgb(var(--color-panel) / 1);
|
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 {
|
.dashboard-action {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
import QRCode from "qrcode";
|
||||||
import { db } from "@/lib/db";
|
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 { ProfileHeader } from "@/components/public/profile-header";
|
||||||
import { PaymentMethodCard } from "@/components/public/payment-method-card";
|
import { PaymentMethodCard } from "@/components/public/payment-method-card";
|
||||||
import { SocialLinksList } from "@/components/public/social-links-list";
|
import { SocialLinksList } from "@/components/public/social-links-list";
|
||||||
@@ -54,6 +56,23 @@ export default async function PublicProfilePage({ params }: PublicProfilePagePro
|
|||||||
notFound();
|
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"];
|
const tokens = THEME_TOKENS[profile.themeId] ?? THEME_TOKENS["terminal-dark"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,7 +105,7 @@ export default async function PublicProfilePage({ params }: PublicProfilePagePro
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{profile.paymentMethods.map((method) => (
|
{methodsWithQr.map((method) => (
|
||||||
<PaymentMethodCard key={method.id} method={method} />
|
<PaymentMethodCard key={method.id} method={method} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
|
||||||
import { PaymentMethod, PaymentMethodType } from "@prisma/client";
|
import { PaymentMethod, PaymentMethodType } from "@prisma/client";
|
||||||
import { PAYMENT_METHOD_LABELS, SUPPORTS_QR } from "@/lib/constants";
|
import { PAYMENT_METHOD_LABELS } from "@/lib/constants";
|
||||||
import { buildPaymentUri } from "@/lib/payment-uri";
|
|
||||||
import { CopyButton } from "@/components/public/copy-button";
|
import { CopyButton } from "@/components/public/copy-button";
|
||||||
import { QrModal } from "@/components/public/qr-modal";
|
|
||||||
|
|
||||||
type PublicPaymentMethod = Pick<
|
type PublicPaymentMethod = Pick<
|
||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
"id" | "type" | "label" | "value" | "network" | "description" | "isVisible"
|
"id" | "type" | "label" | "value" | "network" | "description" | "isVisible"
|
||||||
>;
|
> & {
|
||||||
|
qrPayload: string | null;
|
||||||
|
qrDataUrl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
type PaymentMethodCardProps = {
|
type PaymentMethodCardProps = {
|
||||||
method: PublicPaymentMethod;
|
method: PublicPaymentMethod;
|
||||||
@@ -26,8 +28,7 @@ function isHttpUrl(value: string) {
|
|||||||
|
|
||||||
export function PaymentMethodCard({ method }: PaymentMethodCardProps) {
|
export function PaymentMethodCard({ method }: PaymentMethodCardProps) {
|
||||||
const typeLabel = PAYMENT_METHOD_LABELS[method.type as PaymentMethodType];
|
const typeLabel = PAYMENT_METHOD_LABELS[method.type as PaymentMethodType];
|
||||||
const qrPayload = buildPaymentUri(method.type, method.value, method.network);
|
const linkTarget = method.qrPayload && isHttpUrl(method.qrPayload) ? method.qrPayload : null;
|
||||||
const linkTarget = isHttpUrl(qrPayload) ? qrPayload : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="method-card">
|
<article className="method-card">
|
||||||
@@ -44,7 +45,20 @@ export function PaymentMethodCard({ method }: PaymentMethodCardProps) {
|
|||||||
|
|
||||||
<div className="method-actions">
|
<div className="method-actions">
|
||||||
<CopyButton value={method.value} />
|
<CopyButton value={method.value} />
|
||||||
{SUPPORTS_QR.has(method.type) ? <QrModal title={`${method.label} QR`} payload={qrPayload} /> : null}
|
{method.qrPayload ? (
|
||||||
|
<details className="method-qr-details">
|
||||||
|
<summary className="method-action">QR</summary>
|
||||||
|
<div className="method-qr-panel">
|
||||||
|
<p className="method-qr-title">{method.label} QR</p>
|
||||||
|
{method.qrDataUrl ? (
|
||||||
|
<img src={method.qrDataUrl} alt={`${method.label} QR code`} className="method-qr-image" />
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted">Could not generate QR code for this value.</p>
|
||||||
|
)}
|
||||||
|
<p className="method-qr-payload">{method.qrPayload}</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
{linkTarget ? (
|
{linkTarget ? (
|
||||||
<a href={linkTarget} target="_blank" rel="noreferrer" className="method-action">
|
<a href={linkTarget} target="_blank" rel="noreferrer" className="method-action">
|
||||||
Open
|
Open
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function Modal({ open, title, onClose, children }: ModalProps) {
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[120] grid place-items-center bg-black/80 p-4 md:p-6"
|
className="fixed inset-0 z-[220] grid place-items-center bg-black/80 p-4 md:p-6"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user