onboarding: hide bio behind Advanced toggle; show profile step first for new campaign creators
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Bitcoin,
|
||||
ChevronDown,
|
||||
Download,
|
||||
Eye,
|
||||
EyeOff,
|
||||
@@ -92,7 +93,7 @@ export function OnboardingGate({ children }: { children: ReactNode }) {
|
||||
* per-flow state resets cleanly between sessions. */
|
||||
function CaptiveOverlay() {
|
||||
const { t } = useTranslation();
|
||||
const { cancel, role: contextRole, setRole: setContextRole } = useOnboarding();
|
||||
const { cancel, role: contextRole, setRole: setContextRole, skipToProfile } = useOnboarding();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const login = useLoginActions();
|
||||
@@ -100,13 +101,16 @@ function CaptiveOverlay() {
|
||||
const { mutateAsync: publishEvent, isPending: isPublishingProfile } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploadingAvatar } = useUploadFile();
|
||||
|
||||
// Decide the entry step. Already-authenticated users (e.g. a CTA called
|
||||
// startSignup() on a logged-in surface to walk them to the role picker)
|
||||
// skip keygen / secure / profile and land on `role` directly.
|
||||
// Decide the entry step.
|
||||
// - Already-authenticated users normally land on `role` directly.
|
||||
// - If `skipToProfile` is set (e.g. campaign creation for a user with no
|
||||
// profile), they land on `profile` first; after finishing we navigate
|
||||
// straight to the role destination without showing the role picker.
|
||||
const initialStep: Step = useMemo(() => {
|
||||
if (user && skipToProfile) return 'profile';
|
||||
if (user) return 'role';
|
||||
return 'keygen';
|
||||
}, [user]);
|
||||
}, [user, skipToProfile]);
|
||||
|
||||
const [step, setStep] = useState<Step>(initialStep);
|
||||
|
||||
@@ -227,10 +231,19 @@ function CaptiveOverlay() {
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
goTo('role');
|
||||
// When the overlay was opened with skipToProfile (e.g. from
|
||||
// /campaigns/new for a user without a profile), skip the role
|
||||
// picker and navigate directly to the pre-seeded role destination.
|
||||
if (skipToProfile && contextRole) {
|
||||
cancel();
|
||||
if (contextRole === 'creator') navigate('/campaigns/new');
|
||||
else navigate('/campaigns');
|
||||
} else {
|
||||
goTo('role');
|
||||
}
|
||||
}
|
||||
},
|
||||
[profileData, publishEvent, toast, t, goTo],
|
||||
[profileData, publishEvent, toast, t, goTo, skipToProfile, contextRole, cancel, navigate],
|
||||
);
|
||||
|
||||
// Step renderer -----------------------------------------------------------
|
||||
@@ -577,6 +590,7 @@ function ProfileStep({
|
||||
onSkip,
|
||||
}: ProfileStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2 text-center">
|
||||
@@ -597,20 +611,6 @@ function ProfileStep({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="onb-profile-about" className="text-sm font-medium">
|
||||
{t('onboarding.profile.aboutLabel')}
|
||||
</label>
|
||||
<Textarea
|
||||
id="onb-profile-about"
|
||||
value={data.about}
|
||||
onChange={(e) => onChange({ about: e.target.value })}
|
||||
placeholder={t('onboarding.profile.aboutPlaceholder')}
|
||||
className="resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="onb-profile-picture" className="text-sm font-medium">
|
||||
{t('onboarding.profile.avatarLabel')}
|
||||
@@ -646,6 +646,33 @@ function ProfileStep({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced((v) => !v)}
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn('h-4 w-4 transition-transform duration-200', showAdvanced && 'rotate-180')}
|
||||
/>
|
||||
{t('onboarding.profile.advanced')}
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="onb-profile-about" className="text-sm font-medium">
|
||||
{t('onboarding.profile.aboutLabel')}
|
||||
</label>
|
||||
<Textarea
|
||||
id="onb-profile-about"
|
||||
value={data.about}
|
||||
onChange={(e) => onChange({ about: e.target.value })}
|
||||
placeholder={t('onboarding.profile.aboutPlaceholder')}
|
||||
className="resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
export function OnboardingProvider({ children }: { children: ReactNode }) {
|
||||
const [active, setActive] = useState(false);
|
||||
const [role, setRoleState] = useState<OnboardingRole>(null);
|
||||
const [skipToProfile, setSkipToProfile] = useState(false);
|
||||
|
||||
const startSignup = useCallback((options?: StartSignupOptions) => {
|
||||
setRoleState(options?.role ?? null);
|
||||
setSkipToProfile(options?.skipToProfile ?? false);
|
||||
setActive(true);
|
||||
}, []);
|
||||
|
||||
@@ -37,7 +39,7 @@ export function OnboardingProvider({ children }: { children: ReactNode }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OnboardingContext.Provider value={{ active, role, startSignup, cancel, setRole }}>
|
||||
<OnboardingContext.Provider value={{ active, role, skipToProfile, startSignup, cancel, setRole }}>
|
||||
{children}
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,13 @@ export interface StartSignupOptions {
|
||||
* (e.g. "Start a campaign") can skip the role step by passing this.
|
||||
*/
|
||||
role?: 'creator' | 'donor';
|
||||
/**
|
||||
* When `true` and the user is already logged in, jump directly to the
|
||||
* profile step instead of the role picker. Used by surfaces like
|
||||
* `/campaigns/new` that want the user to set up a profile first.
|
||||
* After the profile step completes the flow navigates based on `role`.
|
||||
*/
|
||||
skipToProfile?: boolean;
|
||||
}
|
||||
|
||||
export interface OnboardingContextValue {
|
||||
@@ -23,6 +30,11 @@ export interface OnboardingContextValue {
|
||||
active: boolean;
|
||||
/** Selected role, or `null` if the picker hasn't run / been skipped. */
|
||||
role: OnboardingRole;
|
||||
/**
|
||||
* When `true`, the overlay was opened with `skipToProfile` so an
|
||||
* already-logged-in user sees the profile step first.
|
||||
*/
|
||||
skipToProfile: boolean;
|
||||
/** Begin the captive signup flow. Optionally pre-seed the role. */
|
||||
startSignup: (options?: StartSignupOptions) => void;
|
||||
/** Cancel and dismiss the overlay. Called from the gate when the user
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"aboutPlaceholder": "نبذة قصيرة عنك…",
|
||||
"avatarLabel": "الصورة الرمزية",
|
||||
"uploadAvatar": "رفع صورة رمزية",
|
||||
"advanced": "متقدّم",
|
||||
"finish": "إنهاء",
|
||||
"saving": "جارٍ الحفظ…",
|
||||
"skip": "تخطٍّ في الوقت الحالي",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "A little about you…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Upload avatar",
|
||||
"advanced": "Advanced",
|
||||
"finish": "Finish",
|
||||
"saving": "Saving…",
|
||||
"skip": "Skip for now",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "Un poco sobre ti…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Subir avatar",
|
||||
"advanced": "Avanzado",
|
||||
"finish": "Finalizar",
|
||||
"saving": "Guardando…",
|
||||
"skip": "Omitir por ahora",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "کمی دربارهٔ شما…",
|
||||
"avatarLabel": "آواتار",
|
||||
"uploadAvatar": "بارگذاری آواتار",
|
||||
"advanced": "پیشرفته",
|
||||
"finish": "پایان",
|
||||
"saving": "در حال ذخیره…",
|
||||
"skip": "فعلاً رد شو",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"aboutPlaceholder": "Un mot sur vous…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Téléverser un avatar",
|
||||
"advanced": "Avancé",
|
||||
"finish": "Terminer",
|
||||
"saving": "Enregistrement…",
|
||||
"skip": "Passer pour l'instant",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "अपने बारे में थोड़ा सा…",
|
||||
"avatarLabel": "अवतार",
|
||||
"uploadAvatar": "अवतार अपलोड करें",
|
||||
"advanced": "उन्नत",
|
||||
"finish": "पूरा करें",
|
||||
"saving": "सेव हो रहा है…",
|
||||
"skip": "अभी छोड़ दें",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "Sedikit tentang Anda…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Unggah avatar",
|
||||
"advanced": "Lanjutan",
|
||||
"finish": "Selesai",
|
||||
"saving": "Menyimpan…",
|
||||
"skip": "Lewati dulu",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "បន្តិចបន្តួចអំពីអ្នក…",
|
||||
"avatarLabel": "រូបតំណាង",
|
||||
"uploadAvatar": "ផ្ទុករូបតំណាង",
|
||||
"advanced": "កម្រិតខ្ពស់",
|
||||
"finish": "បញ្ចប់",
|
||||
"saving": "កំពុងរក្សាទុក…",
|
||||
"skip": "រំលងជាមុនសិន",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "د ځان په اړه یو څه…",
|
||||
"avatarLabel": "اواتار",
|
||||
"uploadAvatar": "اواتار پورته کول",
|
||||
"advanced": "پرمختللي",
|
||||
"finish": "پای",
|
||||
"saving": "خوندي کېږي…",
|
||||
"skip": "اوس یې پرېږدئ",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "Um pouco sobre você…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Enviar avatar",
|
||||
"advanced": "Avançado",
|
||||
"finish": "Concluir",
|
||||
"saving": "Salvando…",
|
||||
"skip": "Pular por enquanto",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "Немного о вас…",
|
||||
"avatarLabel": "Аватар",
|
||||
"uploadAvatar": "Загрузить аватар",
|
||||
"advanced": "Дополнительно",
|
||||
"finish": "Готово",
|
||||
"saving": "Сохранение…",
|
||||
"skip": "Пропустить",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "Zvishoma pamusoro pako…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Isa avatar",
|
||||
"advanced": "Yakadzama",
|
||||
"finish": "Pedzisa",
|
||||
"saving": "Kuchengeta…",
|
||||
"skip": "Pfuura zvinosvika",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"aboutPlaceholder": "Kidogo kukuhusu…",
|
||||
"avatarLabel": "Avatari",
|
||||
"uploadAvatar": "Pakia avatari",
|
||||
"advanced": "Zaidi",
|
||||
"finish": "Maliza",
|
||||
"saving": "Inahifadhi…",
|
||||
"skip": "Ruka kwa sasa",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"aboutPlaceholder": "Kendiniz hakkında birkaç söz…",
|
||||
"avatarLabel": "Avatar",
|
||||
"uploadAvatar": "Avatar yükle",
|
||||
"advanced": "Gelişmiş",
|
||||
"finish": "Bitir",
|
||||
"saving": "Kaydediliyor…",
|
||||
"skip": "Şimdilik atla",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "簡單介紹一下你自己…",
|
||||
"avatarLabel": "頭像",
|
||||
"uploadAvatar": "上傳頭像",
|
||||
"advanced": "進階",
|
||||
"finish": "完成",
|
||||
"saving": "儲存中…",
|
||||
"skip": "暫時略過",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"aboutPlaceholder": "介绍一下你自己……",
|
||||
"avatarLabel": "头像",
|
||||
"uploadAvatar": "上传头像",
|
||||
"advanced": "高级",
|
||||
"finish": "完成",
|
||||
"saving": "正在保存……",
|
||||
"skip": "暂时跳过",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -39,6 +39,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useHdWallet } from '@/hooks/useHdWallet';
|
||||
import { useManageableOrganizations } from '@/hooks/useManageableOrganizations';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import { useOnboarding } from '@/contexts/onboardingContextDef';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { formatBTC, satsToUSD } from '@/lib/bitcoin';
|
||||
import {
|
||||
@@ -131,6 +132,33 @@ export function CreateCampaignPage() {
|
||||
? (userMetadata?.name ?? userMetadata?.display_name ?? genUserName(user.pubkey))
|
||||
: '';
|
||||
|
||||
const { startSignup, active: onboardingActive } = useOnboarding();
|
||||
|
||||
// Track whether we've already triggered the profile gate so the effect
|
||||
// doesn't re-fire on subsequent renders (e.g. when the overlay dismisses
|
||||
// and userAuthor re-renders with data).
|
||||
const profileGateFiredRef = useRef(false);
|
||||
|
||||
// If a logged-in user navigates to /campaigns/new without a kind-0 profile,
|
||||
// open the onboarding profile step so they set a name before the campaign
|
||||
// wizard loads. Only runs once per page mount and only in create mode.
|
||||
const editNaddr = searchParams.get('edit');
|
||||
const isEditMode = !!editNaddr;
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode) return;
|
||||
if (!user) return;
|
||||
if (userAuthor.isLoading) return;
|
||||
if (profileGateFiredRef.current) return;
|
||||
if (onboardingActive) return;
|
||||
|
||||
const hasProfile = !!(userMetadata?.name || userMetadata?.display_name);
|
||||
if (!hasProfile) {
|
||||
profileGateFiredRef.current = true;
|
||||
startSignup({ role: 'creator', skipToProfile: true });
|
||||
}
|
||||
}, [isEditMode, user, userAuthor.isLoading, userMetadata, onboardingActive, startSignup]);
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [story, setStory] = useState('');
|
||||
const [bannerUrl, setBannerUrl] = useState('');
|
||||
@@ -189,9 +217,7 @@ export function CreateCampaignPage() {
|
||||
const [formError, setFormError] = useState('');
|
||||
const [prepopulatedEventId, setPrepopulatedEventId] = useState<string | null>(null);
|
||||
|
||||
const editNaddr = searchParams.get('edit');
|
||||
const editTarget = useMemo(() => getEditTarget(editNaddr), [editNaddr]);
|
||||
const isEditMode = !!editNaddr;
|
||||
|
||||
// ── Organization context (implicit) ────────────────────────────────────
|
||||
const orgParam = searchParams.get('org');
|
||||
|
||||
Reference in New Issue
Block a user