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:
parent
f6abcaf662
commit
9e37308139
6 changed files with 156 additions and 96 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue