From f9465d75c26d30248b9501c01d555983f3bebe13 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Mon, 17 Nov 2025 03:02:34 +0000 Subject: [PATCH] Prettier format pending files --- api/creators.ts | 30 +++++++---- .../components/onboarding/CreatorProfile.tsx | 8 ++- client/lib/identifier-resolver.ts | 19 +++++-- client/pages/Onboarding.tsx | 50 +++++++++++-------- client/pages/ethos/ArtistProfile.tsx | 4 +- docs/USERNAME-FIRST-UUID-FALLBACK.md | 39 +++++++++------ server/index.ts | 32 ++++++++---- 7 files changed, 114 insertions(+), 68 deletions(-) diff --git a/api/creators.ts b/api/creators.ts index a8e5935a..5a1cd864 100644 --- a/api/creators.ts +++ b/api/creators.ts @@ -132,9 +132,15 @@ export async function createCreatorProfile(req: Request, userId: string) { } = body; // Validate required fields - if (!username || typeof username !== "string" || username.trim().length === 0) { + if ( + !username || + typeof username !== "string" || + username.trim().length === 0 + ) { return new Response( - JSON.stringify({ error: "Username is required and must be a non-empty string" }), + JSON.stringify({ + error: "Username is required and must be a non-empty string", + }), { status: 400, headers: { "Content-Type": "application/json" } }, ); } @@ -234,10 +240,7 @@ export async function updateCreatorProfile(req: Request, userId: string) { const normalizedUsername = username.trim().toLowerCase(); - if ( - currentCreator && - currentCreator.username !== normalizedUsername - ) { + if (currentCreator && currentCreator.username !== normalizedUsername) { // Username is being changed, check if new username exists const { data: existingCreator } = await supabase .from("aethex_creators") @@ -265,12 +268,17 @@ export async function updateCreatorProfile(req: Request, userId: string) { if (bio !== undefined) updateData.bio = bio; if (skills !== undefined) updateData.skills = skills; if (avatar_url !== undefined) updateData.avatar_url = avatar_url; - if (experience_level !== undefined) updateData.experience_level = experience_level; + if (experience_level !== undefined) + updateData.experience_level = experience_level; if (primary_arm !== undefined) updateData.primary_arm = primary_arm; - if (arm_affiliations !== undefined) updateData.arm_affiliations = arm_affiliations; - if (is_discoverable !== undefined) updateData.is_discoverable = is_discoverable; - if (allow_recommendations !== undefined) updateData.allow_recommendations = allow_recommendations; - if (spotify_profile_url !== undefined) updateData.spotify_profile_url = spotify_profile_url; + if (arm_affiliations !== undefined) + updateData.arm_affiliations = arm_affiliations; + if (is_discoverable !== undefined) + updateData.is_discoverable = is_discoverable; + if (allow_recommendations !== undefined) + updateData.allow_recommendations = allow_recommendations; + if (spotify_profile_url !== undefined) + updateData.spotify_profile_url = spotify_profile_url; const { data, error } = await supabase .from("aethex_creators") diff --git a/client/components/onboarding/CreatorProfile.tsx b/client/components/onboarding/CreatorProfile.tsx index 076f9051..dffef7b1 100644 --- a/client/components/onboarding/CreatorProfile.tsx +++ b/client/components/onboarding/CreatorProfile.tsx @@ -81,7 +81,10 @@ export default function CreatorProfile({ }; const canProceed = useMemo(() => { - const hasUsername = data?.username && typeof data.username === 'string' && data.username.trim().length > 0; + const hasUsername = + data?.username && + typeof data.username === "string" && + data.username.trim().length > 0; const hasPrimaryArm = creatorData.primaryArm; const hasSkills = creatorData.skills.length > 0; return hasUsername && hasPrimaryArm && hasSkills; @@ -153,7 +156,8 @@ export default function CreatorProfile({ />

- {(data?.username || "").length}/32 characters. Lowercase letters, numbers, hyphens, and underscores only. + {(data?.username || "").length}/32 characters. Lowercase letters, + numbers, hyphens, and underscores only.

{(data?.username?.length || 0) === 0 && (

Username is required

diff --git a/client/lib/identifier-resolver.ts b/client/lib/identifier-resolver.ts index 2e83b449..0efcd173 100644 --- a/client/lib/identifier-resolver.ts +++ b/client/lib/identifier-resolver.ts @@ -11,7 +11,8 @@ const API_BASE = typeof window !== "undefined" ? window.location.origin : ""; * Check if a string is a valid UUID */ export function isUUID(str: string): boolean { - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; return uuidRegex.test(str); } @@ -26,14 +27,18 @@ export async function resolveIdentifierToCreator( try { // Try username first (most common case) - const usernameResponse = await fetch(`${API_BASE}/api/creators/${identifier}`); + const usernameResponse = await fetch( + `${API_BASE}/api/creators/${identifier}`, + ); if (usernameResponse.ok) { return await usernameResponse.json(); } // If username lookup failed and identifier is a UUID, try UUID lookup if (isUUID(identifier)) { - const uuidResponse = await fetch(`${API_BASE}/api/creators/user/${identifier}`); + const uuidResponse = await fetch( + `${API_BASE}/api/creators/user/${identifier}`, + ); if (uuidResponse.ok) { return await uuidResponse.json(); } @@ -49,7 +54,9 @@ export async function resolveIdentifierToCreator( /** * Resolve an identifier to a user by ID */ -export async function resolveIdentifierToUserId(identifier: string): Promise { +export async function resolveIdentifierToUserId( + identifier: string, +): Promise { if (!identifier) return null; if (isUUID(identifier)) { @@ -69,7 +76,9 @@ export async function resolveIdentifierToUserId(identifier: string): Promise { +export async function resolveIdentifierToUsername( + identifier: string, +): Promise { if (!identifier) return null; if (!isUUID(identifier)) { diff --git a/client/pages/Onboarding.tsx b/client/pages/Onboarding.tsx index e2f92da1..9ef7e042 100644 --- a/client/pages/Onboarding.tsx +++ b/client/pages/Onboarding.tsx @@ -321,7 +321,9 @@ export default function Onboarding() { // Use username from form if provided, otherwise generate from first name const usernameFromForm = data.username?.trim().toLowerCase(); - const generatedUsername = normalizedFirst.replace(/\s+/g, "_").toLowerCase(); + const generatedUsername = normalizedFirst + .replace(/\s+/g, "_") + .toLowerCase(); const finalUsername = usernameFromForm || generatedUsername; if (!finalUsername) { @@ -377,23 +379,24 @@ export default function Onboarding() { ); // Create creator profile if they provided primary arm - const creatorProfilePromise = data.creatorProfile.primaryArm && finalUsername - ? fetch(`${API_BASE}/api/creators`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - user_id: user.id, - username: finalUsername, - bio: data.creatorProfile.bio || null, - avatar_url: null, // Can be added later in profile settings - experience_level: data.experience.level || "junior", - primary_arm: data.creatorProfile.primaryArm, - arm_affiliations: [data.creatorProfile.primaryArm], - skills: data.creatorProfile.skills || [], - is_discoverable: true, - }), - }) - : Promise.resolve(); + const creatorProfilePromise = + data.creatorProfile.primaryArm && finalUsername + ? fetch(`${API_BASE}/api/creators`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_id: user.id, + username: finalUsername, + bio: data.creatorProfile.bio || null, + avatar_url: null, // Can be added later in profile settings + experience_level: data.experience.level || "junior", + primary_arm: data.creatorProfile.primaryArm, + arm_affiliations: [data.creatorProfile.primaryArm], + skills: data.creatorProfile.skills || [], + is_discoverable: true, + }), + }) + : Promise.resolve(); // Save followed arms const followedArmsPromises = (data.followedArms || []).map((armId) => @@ -405,7 +408,7 @@ export default function Onboarding() { arm_id: armId, action: "follow", }), - }) + }), ); Promise.allSettled([ @@ -509,7 +512,8 @@ export default function Onboarding() {

- Step {currentStep + 1} of {steps.length} + Step {currentStep + 1} of{" "} + {steps.length}

- {currentStep === 0 && "Choose your path and let's begin your journey"} + {currentStep === 0 && + "Choose your path and let's begin your journey"} {currentStep === 1 && "Help us know you better"} {currentStep === 2 && "Tell us about your experience"} {currentStep === 3 && "What are your interests?"} {currentStep === 4 && "Select your primary focus area"} - {currentStep === 5 && "Follow the arms you want to see in your feed"} + {currentStep === 5 && + "Follow the arms you want to see in your feed"} {currentStep === 6 && "Set up your creator profile"} {currentStep === 7 && "You're ready to go!"}

diff --git a/client/pages/ethos/ArtistProfile.tsx b/client/pages/ethos/ArtistProfile.tsx index 0fdf024e..e528eb83 100644 --- a/client/pages/ethos/ArtistProfile.tsx +++ b/client/pages/ethos/ArtistProfile.tsx @@ -72,7 +72,9 @@ export default function ArtistProfile() { } } - const res = await fetch(`${API_BASE}/api/ethos/artists?id=${resolvedUserId}`); + const res = await fetch( + `${API_BASE}/api/ethos/artists?id=${resolvedUserId}`, + ); const data = await res.json(); setArtist(data); } catch (error) { diff --git a/docs/USERNAME-FIRST-UUID-FALLBACK.md b/docs/USERNAME-FIRST-UUID-FALLBACK.md index dda9059b..a87c6574 100644 --- a/docs/USERNAME-FIRST-UUID-FALLBACK.md +++ b/docs/USERNAME-FIRST-UUID-FALLBACK.md @@ -5,6 +5,7 @@ The entire system now uses **usernames as the primary identifier** with **UUID fallback** for all user/creator lookups across routes and APIs. This means: + - Users visit `/creators/john-doe` (preferred) or `/creators/` (also works) - Users visit `/passport/alice-developer` (preferred) or `/passport/` (also works) - Users visit `/ethos/artists/bob-musician` (preferred) or `/ethos/artists/` (also works) @@ -18,15 +19,16 @@ This means: **New file** that provides helper functions: ```typescript -isUUID(str) // Check if string is UUID format -resolveIdentifierToCreator(id) // Resolve username/UUID → creator object -resolveIdentifierToUserId(id) // Resolve username/UUID → UUID -resolveIdentifierToUsername(id) // Resolve UUID → username +isUUID(str); // Check if string is UUID format +resolveIdentifierToCreator(id); // Resolve username/UUID → creator object +resolveIdentifierToUserId(id); // Resolve username/UUID → UUID +resolveIdentifierToUsername(id); // Resolve UUID → username ``` ### 2. Creators API Endpoint (`code/server/index.ts`) **Updated** `GET /api/creators/:identifier` to: + - Accept username OR UUID in the `:identifier` parameter - Try **username first** (preferred) - Fall back to **UUID lookup** if username lookup fails @@ -41,6 +43,7 @@ GET /api/creators/550e8400-... // ✅ UUID lookup fallback ### 3. Creators Profile Component (`code/client/pages/creators/CreatorProfile.tsx`) **Updated** to: + - Import UUID/identifier resolver helpers - Accept both username and UUID as route parameters - Resolve UUID to username for canonical URL redirect (optional) @@ -49,6 +52,7 @@ GET /api/creators/550e8400-... // ✅ UUID lookup fallback ### 4. Ethos Artist Profile (`code/client/pages/ethos/ArtistProfile.tsx`) **Updated** to: + - Import identifier resolver helpers - Accept both username and userId as route parameters - Resolve username → userId before API call @@ -57,6 +61,7 @@ GET /api/creators/550e8400-... // ✅ UUID lookup fallback ### 5. Passport Profile (`code/client/pages/ProfilePassport.tsx`) **Already supported** username-first with UUID fallback: + - Has built-in `isUuid()` function - Tries `getProfileByUsername()` first - Falls back to `getProfileById()` if username lookup fails @@ -65,6 +70,7 @@ GET /api/creators/550e8400-... // ✅ UUID lookup fallback ### 6. Creator Profile Validation (`code/api/creators.ts`) **Enforced usernames as required**: + - Username must be provided when creating creator profile - Username must be unique (409 Conflict if duplicate) - Username is normalized to lowercase @@ -74,12 +80,12 @@ GET /api/creators/550e8400-... // ✅ UUID lookup fallback ## Routes That Support Username-First with UUID Fallback -| Route | Type | Status | -|-------|------|--------| -| `/creators/:identifier` | Frontend | ✅ Updated | -| `/passport/:identifier` | Frontend | ✅ Already working | -| `/ethos/artists/:identifier` | Frontend | ✅ Updated | -| `/api/creators/:identifier` | Backend | ✅ Updated | +| Route | Type | Status | +| ---------------------------- | -------- | ------------------ | +| `/creators/:identifier` | Frontend | ✅ Updated | +| `/passport/:identifier` | Frontend | ✅ Already working | +| `/ethos/artists/:identifier` | Frontend | ✅ Updated | +| `/api/creators/:identifier` | Backend | ✅ Updated | --- @@ -156,7 +162,8 @@ Return artist data ✅ ```typescript // UUID regex pattern (standard RFC 4122) -const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const uuidPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function isUUID(str: string): boolean { return uuidPattern.test(str); @@ -166,6 +173,7 @@ function isUUID(str: string): boolean { ### Lookup Priority For any identifier: + 1. **If it matches UUID pattern** → Try UUID lookup directly 2. **If it doesn't match UUID pattern** → Try username lookup first, then UUID fallback 3. **If both fail** → Return 404 Not Found @@ -184,12 +192,12 @@ For any identifier: ### Using the Resolver Utilities ```typescript -import { - isUUID, +import { + isUUID, resolveIdentifierToCreator, resolveIdentifierToUserId, - resolveIdentifierToUsername -} from '@/lib/identifier-resolver'; + resolveIdentifierToUsername, +} from "@/lib/identifier-resolver"; // Check if string is UUID if (isUUID(userInput)) { @@ -297,4 +305,3 @@ code/ ✅ **Username-First with UUID Fallback is now implemented across the entire system.** All user-facing routes and APIs prefer usernames while maintaining backward compatibility with UUID-based URLs. Users must have a username to create a profile, ensuring consistent, SEO-friendly URLs throughout the AeThex ecosystem. - diff --git a/server/index.ts b/server/index.ts index e96b16af..74b595d7 100644 --- a/server/index.ts +++ b/server/index.ts @@ -435,7 +435,9 @@ export function createServer() { if (error || !project) { console.log("[Passport Data API] Project not found:", projectSlug); - return res.status(404).json({ error: "Project not found", projectSlug }); + return res + .status(404) + .json({ error: "Project not found", projectSlug }); } return res.json({ @@ -4446,7 +4448,10 @@ export function createServer() { } // Check if identifier is a UUID (username-first, UUID fallback) - const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier); + const isUUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + identifier, + ); let query = adminSupabase .from("aethex_creators") @@ -4477,11 +4482,16 @@ export function createServer() { // If username lookup failed and it's a valid UUID format, try UUID if (error && !isUUID) { - if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier)) { - const { data: creatorByUUID, error: uuidError } = await adminSupabase - .from("aethex_creators") - .select( - ` + if ( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + identifier, + ) + ) { + const { data: creatorByUUID, error: uuidError } = + await adminSupabase + .from("aethex_creators") + .select( + ` id, username, bio, @@ -4493,10 +4503,10 @@ export function createServer() { created_at, updated_at `, - ) - .eq("is_discoverable", true) - .eq("id", identifier) - .single(); + ) + .eq("is_discoverable", true) + .eq("id", identifier) + .single(); if (!uuidError && creatorByUUID) { // Found by UUID, optionally redirect to username canonical URL