Make website replace the bio slot; relax identity requirements
Add a bioField prop to ProfileCard so the editable slot below the name can edit the website instead of the bio. The verifier identity step now edits the website inline (no separate input) and only requires avatar + name; banner and website are optional, with website still https-validated when entered.
This commit is contained in:
@@ -97,6 +97,12 @@ interface ProfileCardProps {
|
||||
showNip05?: boolean;
|
||||
/** Show NIP-58 badge showcase row (default true). */
|
||||
showBadges?: boolean;
|
||||
/**
|
||||
* Which kind-0 field the editable text slot below the name edits.
|
||||
* - `'about'` (default): the bio textarea.
|
||||
* - `'website'`: a single-line website input, replacing the bio entirely.
|
||||
*/
|
||||
bioField?: 'about' | 'website';
|
||||
/** When provided, render an editable profile fields section below bio */
|
||||
extraFields?: ProfileField[];
|
||||
onExtraFieldsChange?: (fields: ProfileField[]) => void;
|
||||
@@ -110,6 +116,7 @@ export function ProfileCard({
|
||||
onRemoveAvatar,
|
||||
showNip05 = true,
|
||||
showBadges = true,
|
||||
bioField = 'about',
|
||||
extraFields,
|
||||
onExtraFieldsChange,
|
||||
}: ProfileCardProps) {
|
||||
@@ -268,9 +275,23 @@ export function ProfileCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bio */}
|
||||
{/* Bio — or, when `bioField` is `'website'`, a website input that
|
||||
takes the bio's place entirely. */}
|
||||
<div className="mt-2">
|
||||
{editable ? (
|
||||
{bioField === 'website' ? (
|
||||
editable ? (
|
||||
<EditableInput
|
||||
value={(metadata.website as string) ?? ''}
|
||||
placeholder="https://your-website.com"
|
||||
onChange={patch('website')}
|
||||
className="text-sm"
|
||||
/>
|
||||
) : metadata.website ? (
|
||||
<p className="text-sm text-muted-foreground leading-relaxed truncate">
|
||||
{metadata.website}
|
||||
</p>
|
||||
) : null
|
||||
) : editable ? (
|
||||
<EditableTextarea
|
||||
value={metadata.about ?? ''}
|
||||
placeholder="Write a short bio…"
|
||||
|
||||
@@ -5,8 +5,6 @@ import { ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { ProfileCard } from '@/components/ProfileCard';
|
||||
import { ImageCropDialog } from '@/components/ImageCropDialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
@@ -49,10 +47,10 @@ interface VerifierIdentityStepProps {
|
||||
* Verifier sub-flow step 1 — the organization's identity.
|
||||
*
|
||||
* Reuses the app's editable {@link ProfileCard} (circular avatar,
|
||||
* rectangular banner, inline name + website fields) plus the shared
|
||||
* {@link ImageCropDialog} for uploads. All four fields — name, website,
|
||||
* avatar, banner — are required before the user can continue. The website
|
||||
* must be a well-formed `https:` URL.
|
||||
* rectangular banner, inline name, and a website field that replaces the bio
|
||||
* slot) plus the shared {@link ImageCropDialog} for uploads. Avatar and name
|
||||
* are required; banner and website are optional. When a website is entered,
|
||||
* it must be a well-formed `https:` URL.
|
||||
*
|
||||
* Nothing is published here; the draft is published as a single kind-0 event
|
||||
* at the end of the sub-flow, so stepping back and forth never republishes.
|
||||
@@ -125,13 +123,13 @@ export function VerifierIdentityStep({
|
||||
);
|
||||
|
||||
// ── Continue gating ──────────────────────────────────────────────────────
|
||||
// Avatar + name are required; banner is optional. Website is optional too,
|
||||
// but if entered it must be a valid https URL.
|
||||
const nameProvided = draft.name.trim().length > 0;
|
||||
const websiteValid = !!sanitizeUrl(draft.website.trim());
|
||||
const websiteTouched = draft.website.trim().length > 0;
|
||||
const avatarProvided = draft.picture.trim().length > 0;
|
||||
const bannerProvided = draft.banner.trim().length > 0;
|
||||
const canContinue =
|
||||
nameProvided && websiteValid && avatarProvided && bannerProvided && !isUploading;
|
||||
const websiteTouched = draft.website.trim().length > 0;
|
||||
const websiteValid = !websiteTouched || !!sanitizeUrl(draft.website.trim());
|
||||
const canContinue = nameProvided && avatarProvided && websiteValid && !isUploading;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -172,41 +170,29 @@ export function VerifierIdentityStep({
|
||||
<ProfileCard
|
||||
metadata={{
|
||||
name: draft.name,
|
||||
website: draft.website,
|
||||
picture: draft.picture,
|
||||
banner: draft.banner,
|
||||
}}
|
||||
onChange={(patch) => {
|
||||
if (patch.name !== undefined) onChange({ name: patch.name });
|
||||
if (patch.website !== undefined) {
|
||||
onChange({ website: patch.website as string });
|
||||
}
|
||||
}}
|
||||
onPickImage={handlePickImage}
|
||||
bioField="website"
|
||||
showNip05={false}
|
||||
showBadges={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Website — a first-class required field for organizations, so it
|
||||
gets its own labeled input rather than living in ProfileCard's
|
||||
collapsible extra-fields section. */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="verifier-org-website" className="text-sm font-medium">
|
||||
{t('onboarding.verifier.identity.websiteLabel')}
|
||||
</Label>
|
||||
<Input
|
||||
id="verifier-org-website"
|
||||
type="url"
|
||||
inputMode="url"
|
||||
value={draft.website}
|
||||
onChange={(e) => onChange({ website: e.target.value })}
|
||||
placeholder="https://your-org.org"
|
||||
aria-required
|
||||
aria-invalid={websiteTouched && !websiteValid}
|
||||
/>
|
||||
{websiteTouched && !websiteValid && (
|
||||
<p className="text-xs text-destructive">
|
||||
{t('onboarding.verifier.identity.websiteInvalid')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Website is optional, but if entered it must be a valid https URL. */}
|
||||
{websiteTouched && !websiteValid && (
|
||||
<p className="text-xs text-destructive">
|
||||
{t('onboarding.verifier.identity.websiteInvalid')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
|
||||
+1
-2
@@ -153,8 +153,7 @@
|
||||
"verifier": {
|
||||
"identity": {
|
||||
"title": "Set up your organization",
|
||||
"subtitle": "Add your organization's name, website, logo, and a banner. Donors will see these on every campaign you verify.",
|
||||
"websiteLabel": "Website",
|
||||
"subtitle": "Add your organization's name and logo. A website and banner are optional. Donors will see these on every campaign you verify.",
|
||||
"websiteInvalid": "Enter a valid website starting with https://",
|
||||
"cropAvatar": "Crop logo",
|
||||
"cropBanner": "Crop banner",
|
||||
|
||||
Reference in New Issue
Block a user