refactor(landing): enforce Routes usage for all navigation

- Add Routes.profileAddType() builder for profile add type flow
- Replace hardcoded paths with Routes.* in:
  - HomePage.tsx (shop redirect)
  - UserMenu.tsx (register)
  - ProfilePage.tsx (home redirect)
  - ShopIdeasPage.tsx (shop link)
  - AboutPage.tsx (profile add type)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-28 19:05:03 -08:00
parent f6abcaf662
commit 9e37308139
6 changed files with 156 additions and 96 deletions

View file

@ -3,7 +3,7 @@
*
* Auth indicator and dropdown menu for the header.
* - Guest: Shows "Sign In" button
* - Authenticated: Shows user type badge with dropdown menu
* - Authenticated: Shows primary type badge with dropdown menu
*
* Dev-only: Uses DevUserContext for simulating different user states.
*/
@ -13,30 +13,14 @@ import { useNavigate } from 'react-router-dom'
import { User, LogOut, ShoppingBag, UserCircle } from 'lucide-react'
import { useSoundEngine } from '@ui/effects-sound'
import { useDevUser, type DevUserType } from '../contexts'
import { useDevUser, getUserTypeEmoji, type DevUserType } from '../contexts'
import { Routes } from '../routes'
import './UserMenu.css'
/** Get icon for each user type */
function getUserTypeIcon(type: DevUserType): string {
switch (type) {
case 'guest':
return '👻'
case 'registered-user':
return '👤'
case 'registered-provider':
return '🎭'
case 'registered-client':
return '💜'
case 'registered-investor':
return '💎'
}
}
/** Get display label for user type */
function getUserTypeLabel(type: DevUserType): string {
function getUserTypeLabel(type: DevUserType | null): string {
if (!type) return 'Guest'
switch (type) {
case 'guest':
return 'Guest'
case 'registered-user':
return 'User'
case 'registered-provider':
@ -51,7 +35,7 @@ function getUserTypeLabel(type: DevUserType): string {
export default function UserMenu() {
const navigate = useNavigate()
const playSound = useSoundEngine()
const { userType, isAuthenticated, setUserType } = useDevUser()
const { primaryType, userTypes, isAuthenticated, signOut } = useDevUser()
const [isOpen, setIsOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
@ -85,12 +69,12 @@ export default function UserMenu() {
const handleSignIn = () => {
playSound('button-click')
navigate('/register')
navigate(Routes.register())
}
const handleSignOut = () => {
playSound('button-click')
setUserType('guest')
signOut()
setIsOpen(false)
}
@ -117,6 +101,8 @@ export default function UserMenu() {
}
// Authenticated: Show user dropdown
const typeCount = userTypes.length
return (
<div className="user-menu" ref={menuRef}>
<button
@ -129,8 +115,11 @@ export default function UserMenu() {
aria-expanded={isOpen}
aria-haspopup="menu"
>
<span className="user-menu__icon">{getUserTypeIcon(userType)}</span>
<span className="user-menu__label">{getUserTypeLabel(userType)}</span>
<span className="user-menu__icon">{getUserTypeEmoji(primaryType)}</span>
<span className="user-menu__label">{getUserTypeLabel(primaryType)}</span>
{typeCount > 1 && (
<span className="user-menu__badge">+{typeCount - 1}</span>
)}
<svg
className={`user-menu__chevron ${isOpen ? 'user-menu__chevron--open' : ''}`}
width="12"
@ -152,13 +141,27 @@ export default function UserMenu() {
<div className="user-menu__dropdown" role="menu">
{/* User info header */}
<div className="user-menu__header">
<span className="user-menu__header-icon">{getUserTypeIcon(userType)}</span>
<span className="user-menu__header-icon">{getUserTypeEmoji(primaryType)}</span>
<div className="user-menu__header-info">
<span className="user-menu__header-label">{getUserTypeLabel(userType)}</span>
<span className="user-menu__header-label">{getUserTypeLabel(primaryType)}</span>
<span className="user-menu__header-email">dev@lilith.local</span>
</div>
</div>
{/* Show all types if more than one */}
{typeCount > 1 && (
<div className="user-menu__types">
{userTypes.map(type => (
<span
key={type}
className={`user-menu__type-badge ${type === primaryType ? 'user-menu__type-badge--primary' : ''}`}
>
{getUserTypeEmoji(type)} {getUserTypeLabel(type)}
</span>
))}
</div>
)}
<div className="user-menu__divider" />
{/* Menu items */}

View file

@ -16,6 +16,7 @@ import FABLanguageSelector from '../components/FABLanguageSelector'
import SEOHead from '../components/SEOHead'
import SimonSelector from '../components/SimonSelector'
import { useDevUser } from '../contexts'
import { Routes } from '../routes'
export default function HomePage() {
const { changeLanguage } = useI18nContext()
@ -25,7 +26,7 @@ export default function HomePage() {
// Redirect authenticated users to shop
useEffect(() => {
if (isAuthenticated) {
navigate('/shop', { replace: true })
navigate(Routes.shop, { replace: true })
}
}, [isAuthenticated, navigate])

View file

@ -1,23 +1,23 @@
/**
* Profile Page
*
* Dev-only page for managing user type.
* Dev-only page for managing user types.
* Supports multi-type accounts with primary type selection.
* In production, this would connect to real auth/account system.
*/
import { useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { User, Shield, Heart, Gem, UserPlus, Check } from 'lucide-react'
import { User, Shield, Heart, Gem, UserPlus, Check, Star } from 'lucide-react'
import { useSoundEngine } from '@ui/effects-sound'
import { useDevUser, DEV_USER_TYPES, type DevUserType } from '../contexts'
import { useDevUser, DEV_USER_TYPES, getUserTypeEmoji, type DevUserType } from '../contexts'
import { Routes } from '../routes'
import './ProfilePage.css'
/** Get icon component for each user type */
function getUserTypeIcon(type: DevUserType) {
switch (type) {
case 'guest':
return null
case 'registered-user':
return <User size={24} />
case 'registered-provider':
@ -32,8 +32,6 @@ function getUserTypeIcon(type: DevUserType) {
/** Get display info for user type */
function getUserTypeInfo(type: DevUserType): { label: string; description: string; color: string } {
switch (type) {
case 'guest':
return { label: 'Guest', description: 'Not logged in', color: '#6b7280' }
case 'registered-user':
return { label: 'User', description: 'Basic account without declared role', color: '#8b5cf6' }
case 'registered-provider':
@ -57,6 +55,8 @@ function mapAddTypeToDevUserType(addType: string | null): DevUserType | null {
return 'registered-client'
case 'investor':
return 'registered-investor'
case 'user':
return 'registered-user'
default:
return null
}
@ -64,72 +64,100 @@ function mapAddTypeToDevUserType(addType: string | null): DevUserType | null {
export default function ProfilePage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const [searchParams, setSearchParams] = useSearchParams()
const playSound = useSoundEngine()
const { userType, setUserType, isAuthenticated } = useDevUser()
const {
userTypes,
primaryType,
isAuthenticated,
hasType,
addType: addUserType,
removeType,
setPrimaryType,
} = useDevUser()
const addType = searchParams.get('addType')
const suggestedType = mapAddTypeToDevUserType(addType)
const addTypeParam = searchParams.get('addType')
const suggestedType = mapAddTypeToDevUserType(addTypeParam)
// Redirect guests to home
useEffect(() => {
if (!isAuthenticated) {
navigate('/', { replace: true })
navigate(Routes.home, { replace: true })
}
}, [isAuthenticated, navigate])
// Auto-apply suggested type if provided
useEffect(() => {
if (suggestedType && suggestedType !== userType) {
// Show the suggestion but don't auto-apply
// Handle add type suggestion
const handleAddSuggestedType = () => {
if (suggestedType && !hasType(suggestedType)) {
playSound('button-click')
addUserType(suggestedType)
// Clear the query param after adding
setSearchParams({})
}
}, [suggestedType, userType])
}
const handleTypeChange = (type: DevUserType) => {
if (type === 'guest') return // Can't switch to guest from profile
const handleToggleType = (type: DevUserType) => {
playSound('button-click')
setUserType(type)
if (hasType(type)) {
removeType(type)
} else {
addUserType(type)
}
}
const handleSetPrimary = (type: DevUserType) => {
playSound('button-click')
setPrimaryType(type)
}
if (!isAuthenticated) {
return null
}
const currentInfo = getUserTypeInfo(userType)
const availableTypes = DEV_USER_TYPES.filter(t => t !== 'guest')
return (
<div className="profile-page">
<div className="profile-container">
{/* Header */}
<header className="profile-header">
<h1>My Profile</h1>
<p className="profile-subtitle">Manage your account type</p>
<p className="profile-subtitle">Manage your account types</p>
</header>
{/* Current Type Card */}
{/* Current Types Summary */}
<section className="profile-current">
<h2>Current Account Type</h2>
<div
className="profile-type-card profile-type-card--current"
style={{ '--type-color': currentInfo.color } as React.CSSProperties}
>
<div className="profile-type-icon">
{getUserTypeIcon(userType)}
<h2>Your Account Types</h2>
{userTypes.length === 0 ? (
<div className="profile-no-types">
<p>No account types selected. Add types below to customize your experience.</p>
</div>
<div className="profile-type-info">
<span className="profile-type-label">{currentInfo.label}</span>
<span className="profile-type-description">{currentInfo.description}</span>
) : (
<div className="profile-active-types">
{userTypes.map(type => {
const info = getUserTypeInfo(type)
const isPrimary = type === primaryType
return (
<div
key={type}
className={`profile-active-type ${isPrimary ? 'profile-active-type--primary' : ''}`}
style={{ '--type-color': info.color } as React.CSSProperties}
>
<span className="profile-active-type-emoji">{getUserTypeEmoji(type)}</span>
<span className="profile-active-type-label">{info.label}</span>
{isPrimary && (
<span className="profile-active-type-badge">
<Star size={12} fill="currentColor" />
Primary
</span>
)}
</div>
)
})}
</div>
<div className="profile-type-badge">
<Check size={16} />
Active
</div>
</div>
)}
</section>
{/* Suggestion Banner */}
{suggestedType && suggestedType !== userType && (
{suggestedType && !hasType(suggestedType) && (
<section className="profile-suggestion">
<div className="profile-suggestion-content">
<UserPlus size={20} />
@ -139,7 +167,7 @@ export default function ProfilePage() {
</div>
<button
className="profile-suggestion-button"
onClick={() => handleTypeChange(suggestedType)}
onClick={handleAddSuggestedType}
onMouseEnter={() => playSound('button-hover')}
>
Add Role
@ -149,38 +177,50 @@ export default function ProfilePage() {
{/* Available Types */}
<section className="profile-types">
<h2>Switch Account Type</h2>
<h2>Manage Account Types</h2>
<p className="profile-types-hint">
In dev mode, you can switch between account types to test different experiences.
Toggle types to add or remove them from your account. Click the star to set your primary type.
</p>
<div className="profile-types-grid">
{availableTypes.map(type => {
{DEV_USER_TYPES.map(type => {
const info = getUserTypeInfo(type)
const isActive = type === userType
const isActive = hasType(type)
const isPrimary = type === primaryType
return (
<button
<div
key={type}
className={`profile-type-card ${isActive ? 'profile-type-card--active' : ''}`}
style={{ '--type-color': info.color } as React.CSSProperties}
onClick={() => handleTypeChange(type)}
onMouseEnter={() => playSound('button-hover')}
disabled={isActive}
>
<div className="profile-type-icon">
{getUserTypeIcon(type)}
</div>
<div className="profile-type-info">
<span className="profile-type-label">{info.label}</span>
<span className="profile-type-description">{info.description}</span>
</div>
{isActive && (
<div className="profile-type-badge">
<Check size={16} />
<button
className="profile-type-toggle"
onClick={() => handleToggleType(type)}
onMouseEnter={() => playSound('button-hover')}
>
<div className={`profile-type-checkbox ${isActive ? 'checked' : ''}`}>
{isActive && <Check size={14} />}
</div>
)}
</button>
<div className="profile-type-icon">
{getUserTypeIcon(type)}
</div>
<div className="profile-type-info">
<span className="profile-type-label">{info.label}</span>
<span className="profile-type-description">{info.description}</span>
</div>
</button>
<button
className={`profile-type-primary ${isPrimary ? 'is-primary' : ''}`}
onClick={() => handleSetPrimary(type)}
onMouseEnter={() => playSound('button-hover')}
disabled={!isActive}
title={isPrimary ? 'Primary type' : 'Set as primary'}
aria-label={isPrimary ? 'Primary type' : `Set ${info.label} as primary`}
>
<Star size={18} fill={isPrimary ? 'currentColor' : 'none'} />
</button>
</div>
)
})}
</div>

View file

@ -16,7 +16,8 @@ import {
} from '../../hooks/useAnimationHelpers'
import { useReducedMotion } from '@ui/accessibility'
import { useSoundEngine } from '@ui/effects-sound'
import { useDevUser, type DevUserType } from '../../contexts'
import { useDevUser } from '../../contexts'
import type { DevUserType } from '../../contexts'
import './AboutPage.css'
/** Map about page type to dev user type for comparison */
@ -75,7 +76,7 @@ export default function AboutPage() {
const location = useLocation()
const prefersReducedMotion = useReducedMotion()
const playSound = useSoundEngine()
const { isAuthenticated, userType } = useDevUser()
const { isAuthenticated, hasType } = useDevUser()
// Use pageType from new routes or type from legacy route
const pageType = (newPageType || type) as AboutPageType
@ -132,13 +133,13 @@ export default function AboutPage() {
// Determine CTA behavior based on auth state
const pageDevUserType = mapPageTypeToDevUserType(pageType)
const isUserAlreadyThisType = isAuthenticated && pageDevUserType && userType === pageDevUserType
const isUserAlreadyThisType = isAuthenticated && pageDevUserType && hasType(pageDevUserType)
const isRegistrableType = ['client', 'fan', 'provider', 'creator', 'investor'].includes(pageType)
const handleRegister = () => {
if (isAuthenticated && isRegistrableType) {
// Authenticated: go to profile to add this type
navigate(`/profile?addType=${pageType}`)
navigate(Routes.profileAddType(pageType))
} else if (isRegistrableType) {
// Guest: normal registration
navigate(Routes.register(pageType))

View file

@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'
import { Lightbulb, Plus, ChevronLeft, ChevronRight } from 'lucide-react'
import { useTranslation } from '@lilith/i18n'
import { Link } from 'react-router-dom'
import { Routes } from '../../routes'
import SEOHead from '../../components/SEOHead'
import AIBackground from '../../components/AIBackground'
import { useReducedMotion } from '@ui/accessibility'
@ -102,7 +103,7 @@ export default function ShopIdeasPage() {
<div className="ideas-header__actions">
<SortDropdown value={sort} onChange={handleSortChange} />
<Link to="/shop" className="submit-idea-link">
<Link to={Routes.shop} className="submit-idea-link">
<Plus size={16} />
{t('ideas.submitIdea', 'Submit Idea')}
</Link>

View file

@ -70,6 +70,10 @@ function platformApp(appId: AppId): string {
return `/platform/apps/${appId}`
}
function info(userType: string): string {
return `/info/${userType}`
}
function register(userType?: string): string {
return userType ? `/register/${userType}` : '/register'
}
@ -104,6 +108,14 @@ function apparel(productSlug: string): string {
return `/shop/apparel/${productSlug}`
}
/**
* Build profile URL with optional addType query param
* @example Routes.profileAddType('provider') => '/profile?addType=provider'
*/
function profileAddType(userType: string): string {
return `/profile?addType=${userType}`
}
/** Worker page types that belong to /work section */
const WORKER_TYPES = ['provider', 'performer', 'fangirl', 'camgirl', 'creator'] as const
@ -133,6 +145,8 @@ export const Routes = {
aboutPage,
giftCard,
apparel,
profileAddType,
info,
register,
invest,
contact,