Skip to content

Onboarding

RoutePurposeAuth Required
/onboardingNew user setup (2 steps)Yes

Users are redirected here by the checkOnboardingCompleted() guard if onboardingCompleted !== true.


Users choose their primary purpose on the platform. Single select (click to select, click again to deselect).

Goal IDTitleDescriptionDashboard Template
buyerBuyer”I want to discover, support, and purchase from creators and businesses.”buyer_default
sellerSeller”I want to build a site and sell products, services, or digital offerings.”seller_default
refer-and-earnRefer and earn”I want to share Mindhyv and earn recurring commissions.”referral_default
project-managementProject management”I want to organize, manage, and collaborate on structured projects.”pm_default
  1. Dashboard ID mapped from goal
  2. dashboardManager.ensureDashboard() creates dashboard at users/{uid}/dashboards/{dashboardId}
  3. Dashboard populated with template widgets (see Dashboard)
  4. Step advances to 2

Two-panel layout: Form (left) + Live Preview (right).

FieldTypeRequiredValidationNotes
displayNametextYesNon-emptyFull name
userNametextYes1-15 chars, uniqueChecked against Firestore on submit
locationdropdownYesMust selectCountry selector (249+ countries)
timeZonedropdownYesMust select150+ timezones
languagedropdownYesMust select100+ languages
birthDatedate pickerYesValid date, age >= 18Calendar picker, years: current-18 to current-100
photoURLimage uploadNoJPEG/PNG/GIF/WebP, max 5MBWith crop dialog
taglinetextNoMax 30 chars
aboutMetextareaNoMax 500 chars
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age >= 18;

Error: “You must be at least 18 years old.”

Queries users collection where userName == value. Only triggers if username changed from initial value. Error: “Username is already taken. Please choose a different username.”

SettingValue
Crop size320x320 px (fixed square)
Min zoom0.2x (20%)
Max zoom3x (300%)
Zoom step0.05 increments
Max file size5MB
Accepted formatsJPEG, PNG, GIF, WebP
Storage pathusers/{uid}/avatar/avatar_{timestamp}.{ext}

Crop parameters saved: avatarCropX, avatarCropY, avatarCropScale, avatarCropSize, avatarImageWidth, avatarImageHeight

  • Avatar (cropped or initials)
  • Display name (or “Full Name” placeholder)
  • Username (or “@username” placeholder)
  • Age calculated from birthDate
  • Country, timezone, language (with flag)
  • Tagline and About Me text

users/{uid} (update):

{
displayName, userName, location, timeZone, language,
birthDate: Timestamp, // Parsed from YYYY-MM-DD
photoURL?, // If provided
avatarCrop*?, // If crop params != null
onboardingCompleted: true, // Completion flag
updatedAt: Timestamp.now()
}

profiles/{uid} (set with merge):

{
tagline: string,
aboutMe?: string // If not null
}
  • Toast: “Profile updated”
  • Navigate to / with replaceState: true
  • User is now fully onboarded and guard passes

BlockerDescription
Goal not selectedContinue button disabled
Required fields emptySubmit button disabled
Age < 18Submit blocked with error
Username takenSubmit blocked with error
File too largeUpload silently rejected

When ensureDashboard() runs, it creates a dashboard document at users/{uid}/dashboards/{dashboardId}:

{
userId: uid,
name: 'My Dashboard',
layoutVersion: 1,
grid: WidgetInstance[], // From template with new UUIDs
updatedAt: serverTimestamp()
}
GoalTemplateWidgets
Buyerbuyer_defaultInbox, Active Projects, Recommended Sellers
Sellerseller_defaultSales Summary, Orders, Messages, Payouts
Refer & Earnreferral_defaultReferral Stats, Campaign Links, Payouts
Project Managementpm_defaultProject Board, Milestones, Team Chat