SkateSpots/src/components/ShareProfileDialog.tsx
shakespeare.diy 8ba54273b7 ShareProfileDialog: no horizontal scroll + canonical share URL
- Dialog now sizes to the viewport (w-[calc(100vw-2rem)] max-w-sm) with
  overflow-hidden and min-w-0 children so the QR + URL never force lateral scroll
- Slightly smaller QR (48/52) to fit comfortably in the narrow dialog
- Share links/QRs now always anchor to https://skate-spots.org instead of
  window.location.origin, which in the editor resolves to the sandbox iframe host
  (the *.iframe.diy URL you saw)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:16:09 +07:00

128 lines
5 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Check, Copy, Share2, Loader2 } from 'lucide-react';
import { useToast } from '@/hooks/useToast';
import { generateQRCode } from '@/lib/wallet/qrCodeGenerator';
interface ShareProfileDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** The npub (or nprofile) to build the shareable URL from. */
npub: string;
/** Display name, used in the share sheet + QR caption. */
displayName: string;
}
/**
* Share a skater's profile IRL or online. Skateboarding is a physical, local
* community — a QR you can flash at a spot (or slap on a sticker) is worth more
* than a raw npub. Falls back gracefully to copy-link when the native share
* sheet isn't available.
*/
export function ShareProfileDialog({ open, onOpenChange, npub, displayName }: ShareProfileDialogProps) {
const { toast } = useToast();
const [qr, setQr] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
// Always anchor to the canonical public site so shared links/QRs point to the
// real domain — never the preview/sandbox iframe host that window.location
// would give us inside the editor.
const CANONICAL_ORIGIN = 'https://skate-spots.org';
const shareUrl = `${CANONICAL_ORIGIN}/${npub}`;
useEffect(() => {
if (!open) return;
let active = true;
setQr(null);
// qrcode must be imported lazily (see generateQRCode) — a top-level import
// crashes on its canvas dependency at module load.
generateQRCode(shareUrl)
.then((url) => { if (active) setQr(url); })
.catch((err) => {
console.error('[ShareProfileDialog] QR generation failed:', err);
if (active) setQr(null);
});
return () => { active = false; };
}, [open, shareUrl]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
toast({ title: 'Copy failed', description: 'Could not copy the link.', variant: 'destructive' });
}
};
const canNativeShare = typeof navigator !== 'undefined' && !!navigator.share;
const handleNativeShare = async () => {
try {
await navigator.share({
title: `${displayName} on SkateSpots`,
text: `Check out ${displayName} on SkateSpots 🛹`,
url: shareUrl,
});
} catch {
// user cancelled or unsupported — no-op
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[calc(100vw-2rem)] max-w-sm overflow-hidden bg-black border-white/10">
<DialogHeader className="min-w-0">
<DialogTitle className="flex items-center gap-2 uppercase font-black">
<Share2 className="h-5 w-5 text-[#ff0040] flex-shrink-0" />
Share profile
</DialogTitle>
<DialogDescription className="text-gray-400">
Point a phone camera at the code to open {displayName}'s profile.
</DialogDescription>
</DialogHeader>
{/* QR */}
<div className="flex flex-col items-center gap-4 py-2 min-w-0 w-full">
<div className="rounded-2xl bg-white p-3 shadow-lg">
{qr ? (
<img src={qr} alt="Profile QR code" className="w-48 h-48 sm:w-52 sm:h-52" />
) : (
<div className="w-48 h-48 sm:w-52 sm:h-52 flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div>
)}
</div>
{/* URL + copy */}
<button
onClick={handleCopy}
className="group w-full max-w-full flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2.5 hover:border-[#ff0040]/40 transition-colors"
>
<span className="flex-1 min-w-0 text-left text-sm text-gray-300 truncate font-mono">
{shareUrl.replace(/^https?:\/\//, '')}
</span>
{copied ? (
<Check className="h-4 w-4 text-[#ccff00] flex-shrink-0" />
) : (
<Copy className="h-4 w-4 text-gray-500 group-hover:text-white flex-shrink-0" />
)}
</button>
<div className="grid grid-cols-1 gap-2 w-full">
<Button onClick={handleCopy} variant="outline" className="w-full border-white/15 font-bold">
{copied ? <><Check className="h-4 w-4 mr-2 text-[#ccff00]" />Copied!</> : <><Copy className="h-4 w-4 mr-2" />Copy link</>}
</Button>
{canNativeShare && (
<Button onClick={handleNativeShare} className="w-full bg-[#ff0040] hover:bg-[#ff0040]/90 font-black uppercase">
<Share2 className="h-4 w-4 mr-2" />
Share…
</Button>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}