From ff3804777a59dca8840221d16c8155e503d5e6de Mon Sep 17 00:00:00 2001 From: lemon Date: Fri, 12 Jun 2026 20:08:00 -0700 Subject: [PATCH] Add 'Paste URL' option to org setup avatar and banner pickers --- src/components/ProfileCard.tsx | 119 +++++++++++++----- .../onboarding/VerifierIdentityStep.tsx | 30 +++++ src/locales/en.json | 4 +- 3 files changed, 122 insertions(+), 31 deletions(-) diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index b4f7b32a..29f14aa5 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import type { NostrMetadata } from '@nostrify/nostrify'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { CheckCircle2, Pencil, Plus, Trash2, ChevronDown, ImagePlus, X as XIcon } from 'lucide-react'; +import { CheckCircle2, Pencil, Plus, Trash2, ChevronDown, ImagePlus, X as XIcon, Link as LinkIcon } from 'lucide-react'; import { genUserName } from '@/lib/genUserName'; import { BioContent } from '@/components/BioContent'; import { cn } from '@/lib/utils'; @@ -91,6 +91,13 @@ interface ProfileCardProps { metadata: Partial; onChange?: (patch: Partial) => void; onPickImage?: (field: 'picture' | 'banner') => void; + /** + * Called when the user chooses "Paste URL" for an image field. The handler + * is expected to read the clipboard, validate the URL, and apply it. When + * provided, the banner gains a dropdown menu (matching the avatar) so the + * paste action is reachable for both images. + */ + onPasteUrl?: (field: 'picture' | 'banner') => void; /** Called when user removes their avatar picture. */ onRemoveAvatar?: () => void; /** Show NIP-05 row (default true) */ @@ -113,6 +120,7 @@ export function ProfileCard({ metadata, onChange, onPickImage, + onPasteUrl, onRemoveAvatar, showNip05 = true, showBadges = true, @@ -148,36 +156,81 @@ export function ProfileCard({
{/* Banner */} -
editable && onPickImage?.('banner')} - > - {!metadata.banner &&
} - {editable && !metadata.banner && ( -
- -
- )} - {editable && ( - <> -
- - {metadata.banner ? 'Change banner' : 'Add banner'} - -
- {metadata.banner && ( -
- + {editable && onPasteUrl ? ( + // With a paste handler, the banner opens a menu (Change / Paste URL) + // instead of going straight to the file picker. + + + + + + onPickImage?.('banner')}> + + {metadata.banner ? 'Change banner' : 'Add banner'} + + onPasteUrl?.('banner')}> + + Paste URL + + + + ) : ( +
editable && onPickImage?.('banner')} + > + {!metadata.banner &&
} + {editable && !metadata.banner && ( +
+ +
+ )} + {editable && ( + <> +
+ + {metadata.banner ? 'Change banner' : 'Add banner'} + +
+ {metadata.banner && ( +
+ +
+ )} + + )} +
+ )} {/* Profile info */}
@@ -209,6 +262,12 @@ export function ProfileCard({ Change avatar + {onPasteUrl && ( + onPasteUrl?.('picture')}> + + Paste URL + + )} {metadata.picture && ( onRemoveAvatar?.()} className="text-destructive focus:text-destructive"> diff --git a/src/components/onboarding/VerifierIdentityStep.tsx b/src/components/onboarding/VerifierIdentityStep.tsx index e881a37b..a2a64f81 100644 --- a/src/components/onboarding/VerifierIdentityStep.tsx +++ b/src/components/onboarding/VerifierIdentityStep.tsx @@ -77,6 +77,35 @@ export function VerifierIdentityStep({ fileInputRef.current?.click(); }, []); + // Read an image URL from the clipboard, validate it, and apply it directly + // to the draft field. Hosted images are used as-is (no crop/upload step). + const handlePasteUrl = useCallback( + async (field: CropField) => { + let text = ''; + try { + text = (await navigator.clipboard.readText()).trim(); + } catch { + toast({ + title: t('onboarding.verifier.identity.clipboardFailed'), + variant: 'destructive', + }); + return; + } + + const url = sanitizeUrl(text); + if (!url) { + toast({ + title: t('onboarding.verifier.identity.pasteUrlInvalid'), + variant: 'destructive', + }); + return; + } + + onChange({ [field]: url }); + }, + [onChange, t, toast], + ); + const handleFileChosen = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -181,6 +210,7 @@ export function VerifierIdentityStep({ } }} onPickImage={handlePickImage} + onPasteUrl={handlePasteUrl} bioField="website" showNip05={false} showBadges={false} diff --git a/src/locales/en.json b/src/locales/en.json index 38967113..de8941c7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -157,7 +157,9 @@ "websiteInvalid": "Enter a valid website starting with https://", "cropAvatar": "Crop logo", "cropBanner": "Crop banner", - "uploading": "Uploading image…" + "uploading": "Uploading image…", + "clipboardFailed": "Couldn't read from clipboard.", + "pasteUrlInvalid": "Clipboard doesn't contain a valid https URL." }, "bio": { "title": "Tell us about your organization",