feat(frontend): optimize navigation and add providers routes

This commit is contained in:
Lilith 2026-01-10 10:13:32 -08:00
parent f32decdabd
commit 594444f432
10 changed files with 479 additions and 108 deletions

View file

@ -15,7 +15,7 @@ import { lazy, Suspense } from 'react'
import { usePageViewTracking } from '@lilith/analytics-client/react'
import { AgeGateProvider } from '@lilith/age-verification-react'
import { ToastProvider } from '@lilith/ui-feedback'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { NotFoundPage } from '@lilith/ui-error-pages'
import { MotionProvider } from './providers/MotionProvider'
@ -257,10 +257,6 @@ function AppRoutes() {
<Route path={RoutePatterns.contact} element={<HomePage />} />
<Route path={RoutePatterns.newsletter} element={<HomePage />} />
{/* Legacy redirects for marketplace compatibility */}
<Route path="/for-workers" element={<Navigate to="/work" replace />} />
<Route path="/for-clients" element={<Navigate to="/customer" replace />} />
{/* 404 catch-all - must be last */}
<Route
path="*"
@ -269,8 +265,8 @@ function AppRoutes() {
homeLink="/"
homeLinkText="Back to Home"
suggestions={[
'Check our work opportunities at /work',
'Browse as a customer at /customer',
'Check our provider opportunities at /providers',
'Browse as a client at /clients',
'Explore our platform at /platform'
]}
/>

View file

@ -67,8 +67,8 @@ export default function Header({ pageType }: HeaderProps) {
},
{
label: t('navigation.forWorkers'),
href: Routes.work,
onClick: () => handleNavClick(Routes.work),
href: Routes.providers,
onClick: () => handleNavClick(Routes.providers),
children: [
{
label: (
@ -76,8 +76,8 @@ export default function Header({ pageType }: HeaderProps) {
{t('navigation.providers')} <Badge variant="primary" size="sm">v1</Badge>
</>
),
href: Routes.workProvider,
onClick: () => handleNavClick(Routes.workProvider, 'provider'),
href: Routes.providersEscort,
onClick: () => handleNavClick(Routes.providersEscort, 'escort'),
},
{
label: (
@ -85,8 +85,8 @@ export default function Header({ pageType }: HeaderProps) {
{t('navigation.performers')} <Badge variant="primary" size="sm">v3</Badge>
</>
),
href: Routes.workPerformer,
onClick: () => handleNavClick(Routes.workPerformer, 'performer'),
href: Routes.providersPerformer,
onClick: () => handleNavClick(Routes.providersPerformer, 'performer'),
},
{
label: (
@ -94,8 +94,8 @@ export default function Header({ pageType }: HeaderProps) {
{t('navigation.fangirls')} <Badge variant="primary" size="sm">v7</Badge>
</>
),
href: Routes.workFangirl,
onClick: () => handleNavClick(Routes.workFangirl, 'fangirl'),
href: Routes.providersFangirl,
onClick: () => handleNavClick(Routes.providersFangirl, 'fangirl'),
},
{
label: (
@ -103,25 +103,25 @@ export default function Header({ pageType }: HeaderProps) {
{t('navigation.camgirls')} <Badge variant="primary" size="sm">v11</Badge>
</>
),
href: Routes.workCamgirl,
onClick: () => handleNavClick(Routes.workCamgirl, 'camgirl'),
href: Routes.providersCamgirl,
onClick: () => handleNavClick(Routes.providersCamgirl, 'camgirl'),
},
],
},
{
label: t('navigation.forCustomers'),
href: Routes.customer,
onClick: () => handleNavClick(Routes.customer),
href: Routes.clients,
onClick: () => handleNavClick(Routes.clients),
children: [
{
label: t('navigation.clients'),
href: Routes.customerClient,
onClick: () => handleNavClick(Routes.customerClient, 'client'),
href: Routes.clientsBooking,
onClick: () => handleNavClick(Routes.clientsBooking, 'booking'),
},
{
label: t('navigation.fans'),
href: Routes.customerFan,
onClick: () => handleNavClick(Routes.customerFan, 'fan'),
href: Routes.clientsFan,
onClick: () => handleNavClick(Routes.clientsFan, 'fan'),
},
],
},

View file

@ -14,15 +14,15 @@ interface SEOHeadProps {
const BASE_URL = urls.base
/** Category pages that map to category landing routes */
const CATEGORY_PAGES: CategoryPageType[] = ['work', 'customer', 'platform', 'company', 'shop']
const CATEGORY_PAGES: CategoryPageType[] = ['providers', 'clients', 'platform', 'company', 'shop']
/** Map page types to their canonical paths */
function getCanonicalUrl(pageType: SEOPageType): string {
// Category landing pages
if (CATEGORY_PAGES.includes(pageType as CategoryPageType)) {
const categoryPaths: Record<CategoryPageType, string> = {
work: Routes.work,
customer: Routes.customer,
providers: Routes.providers,
clients: Routes.clients,
platform: Routes.platform,
company: Routes.company,
shop: Routes.shop,

View file

@ -27,7 +27,7 @@ function CustomerCard({ category, index }: { category: CustomerCategory; index:
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
>
<Link
to={Routes.customerPage(category.type)}
to={Routes.client(category.type)}
className="category-card"
style={{
'--card-color': category.color,

View file

@ -28,7 +28,7 @@ function WorkerCard({ category, index }: { category: WorkerCategory; index: numb
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
>
<Link
to={Routes.worker(category.type)}
to={Routes.provider(category.type)}
className="category-card"
style={{
'--card-color': category.color,

View file

@ -0,0 +1,222 @@
/**
* ClientRoutes - Client/Consumer route tree
*
* Complete route structure for clients (customers, fans, etc.)
* All routes are prefixed with /client
*
* Funnel stages tracked:
* 1. /client (landing) - FUNNEL_VISIT with audience='client'
* 2. /client/register - FUNNEL_SIGNUP with userType='client'
* 3. /client/subscriptions - FUNNEL_SUBSCRIBE
* 4. /client/book/:id - First booking = FUNNEL_PURCHASE
*/
import { Route } from 'react-router-dom';
import { lazy } from 'react';
import { RequireAuth } from '../components/RequireAuth';
// Landing & public pages
const ClientLandingPage = lazy(
() => import('@features/landing/pages/ClientLandingPage')
);
const ClientAboutPage = lazy(
() => import('@features/client/pages/ClientAboutPage')
);
const ClientFeaturesPage = lazy(
() => import('@features/client/pages/ClientFeaturesPage')
);
const ClientSafetyPage = lazy(
() => import('@features/client/pages/ClientSafetyPage')
);
// Profile viewing (public)
const ProfileViewPage = lazy(() =>
import('@lilith/profile/pages').then((m) => ({ default: m.ProfileViewPage }))
);
// Auth
const RegisterPage = lazy(() => import('@features/auth/pages/RegisterPage'));
// Discovery (authenticated)
const BrowseCreatorsPage = lazy(() =>
import('@features/discovery/pages/BrowseCreatorsPage').then((m) => ({
default: m.BrowseCreatorsPage,
}))
);
const NearbyMapPage = lazy(
() => import('@features/discovery/pages/NearbyMapPage')
);
// Booking & interaction
const BookingPage = lazy(() => import('@features/booking/pages/BookingPage'));
const MessagingPage = lazy(
() => import('@features/messaging/pages/MessagingPage')
);
// Subscription
const SubscriptionCheckoutPage = lazy(() =>
import('@features/subscription/pages/SubscriptionCheckoutPage').then((m) => ({
default: m.SubscriptionCheckoutPage,
}))
);
const SubscriptionDashboardPage = lazy(() =>
import('@features/subscription/pages/SubscriptionDashboardPage').then((m) => ({
default: m.SubscriptionDashboardPage,
}))
);
// Account
const FavoritesPage = lazy(
() => import('@features/client/pages/FavoritesPage')
);
const SettingsPage = lazy(
() => import('@features/client/pages/SettingsPage')
);
/**
* Client routes - /client/* tree
*
* Public routes accessible without auth.
* Protected routes require authentication + client role.
*/
export function ClientRoutes() {
return (
<>
{/* ========================================
PUBLIC CLIENT ROUTES
======================================== */}
{/* Client landing - entry point after audience selection */}
<Route path="/client" element={<ClientLandingPage />} />
{/* Info pages - client-specific content */}
<Route path="/client/about" element={<ClientAboutPage />} />
<Route path="/client/features" element={<ClientFeaturesPage />} />
<Route path="/client/safety" element={<ClientSafetyPage />} />
{/* Public profile viewing */}
<Route path="/client/creators/:username" element={<ProfileViewPage />} />
{/* Registration - prefilled as client */}
<Route
path="/client/register"
element={<RegisterPage defaultRole="client" />}
/>
{/* ========================================
PROTECTED CLIENT ROUTES
Require auth + client role
======================================== */}
{/* Browse creators - main discovery */}
<Route
path="/client/browse"
element={
<RequireAuth
requiredRole="client"
promptTitle="Sign in to browse"
promptDescription="Create an account to discover verified providers."
>
<BrowseCreatorsPage />
</RequireAuth>
}
/>
<Route
path="/client/browse/:vertical"
element={
<RequireAuth requiredRole="client">
<BrowseCreatorsPage />
</RequireAuth>
}
/>
{/* Nearby map - location-based discovery */}
<Route
path="/client/nearby"
element={
<RequireAuth requiredRole="client">
<NearbyMapPage />
</RequireAuth>
}
/>
{/* Favorites */}
<Route
path="/client/favorites"
element={
<RequireAuth requiredRole="client">
<FavoritesPage />
</RequireAuth>
}
/>
{/* Booking */}
<Route
path="/client/book/:providerId"
element={
<RequireAuth
requiredRole="client"
promptTitle="Sign in to book"
promptDescription="Create an account to book with providers."
>
<BookingPage />
</RequireAuth>
}
/>
{/* Messaging - client context (sending inquiries) */}
<Route
path="/client/messages"
element={
<RequireAuth
requiredRole="client"
promptTitle="Sign in to message"
promptDescription="Create an account to send and receive messages."
>
<MessagingPage context="client" />
</RequireAuth>
}
/>
<Route
path="/client/messages/:conversationId"
element={
<RequireAuth requiredRole="client">
<MessagingPage context="client" />
</RequireAuth>
}
/>
{/* Subscriptions */}
<Route
path="/client/subscriptions"
element={
<RequireAuth requiredRole="client">
<SubscriptionDashboardPage />
</RequireAuth>
}
/>
<Route
path="/client/subscriptions/checkout"
element={
<RequireAuth
requiredRole="client"
promptTitle="Sign in to subscribe"
promptDescription="Create an account to access premium features."
>
<SubscriptionCheckoutPage />
</RequireAuth>
}
/>
{/* Settings */}
<Route
path="/client/settings"
element={
<RequireAuth requiredRole="client">
<SettingsPage />
</RequireAuth>
}
/>
</>
);
}

View file

@ -1,91 +1,40 @@
/**
* PublicRoutes - Guest-accessible routes
* PublicRoutes - Root entry point
*
* Routes that don't require authentication.
* These are available to all visitors.
* The root URL redirects to /choose-your-journey where users select their path:
* - Worker /worker/* tree
* - Client /client/* tree
*
* All content, features, and authenticated routes are within the audience-specific trees.
*/
import { Route, Navigate } from 'react-router-dom';
import { lazy } from 'react';
// Landing pages
const HomeRedirect = lazy(() => import('@features/landing/components/HomeRedirect'));
const AudienceChoiceScreen = lazy(() => import('@features/landing/pages/AudienceChoiceScreen'));
const WorkerLandingPage = lazy(() => import('@features/landing/pages/WorkerLandingPage'));
const ClientLandingPage = lazy(() => import('@features/landing/pages/ClientLandingPage'));
const VerticalLandingPage = lazy(() => import('@features/landing/pages/VerticalLandingPage'));
// Content pages
const AboutPage = lazy(() => import('@features/content/pages/AboutPage'));
const FeaturesPage = lazy(() => import('@features/content/pages/FeaturesPage'));
const SafetyPage = lazy(() => import('@features/content/pages/SafetyPage'));
// Auth pages
const RegisterPage = lazy(() => import('@features/auth/pages/RegisterPage'));
// Subscription public pages
const SubscribeHomePage = lazy(() =>
import('@features/subscription/pages/public/SubscribeHomePage').then((m) => ({
default: m.SubscribeHomePage,
}))
);
const SubscribePricingPage = lazy(() =>
import('@features/subscription/pages/public/SubscribePricingPage').then((m) => ({
default: m.SubscribePricingPage,
}))
);
const SubscribeHowItWorksPage = lazy(() =>
import('@features/subscription/pages/public/SubscribeHowItWorksPage').then((m) => ({
default: m.SubscribeHowItWorksPage,
}))
// Entry point
const AudienceChoiceScreen = lazy(
() => import('@features/landing/pages/AudienceChoiceScreen')
);
// Profile (public viewing)
const ProfileViewPage = lazy(() =>
import('@lilith/profile/pages').then((m) => ({ default: m.ProfileViewPage }))
// Vertical-specific landing (can be accessed without audience choice)
const VerticalLandingPage = lazy(
() => import('@features/landing/pages/VerticalLandingPage')
);
/**
* Public route definitions
* These routes are accessible without authentication
* Root routes - entry point to the platform
*/
export function PublicRoutes() {
return (
<>
{/* Home: Auth-aware redirect */}
<Route path="/" element={<HomeRedirect />} />
{/* Root: Always redirect to audience choice */}
<Route path="/" element={<Navigate to="/choose-your-journey" replace />} />
{/* Audience choice screen (for unauthenticated visitors) */}
{/* Audience choice screen - THE entry point */}
<Route path="/choose-your-journey" element={<AudienceChoiceScreen />} />
{/* Direct audience routes (SEO + explicit navigation) */}
<Route path="/for-workers" element={<WorkerLandingPage />} />
<Route path="/for-clients" element={<ClientLandingPage />} />
{/* Vertical-specific landing (direct access) */}
{/* Vertical-specific landing (SEO entry points) */}
<Route path="/vertical/:verticalSlug" element={<VerticalLandingPage />} />
{/* Content pages */}
<Route path="/about" element={<AboutPage />} />
<Route path="/features" element={<FeaturesPage />} />
<Route path="/safety" element={<SafetyPage />} />
{/* Auth pages */}
<Route path="/register" element={<RegisterPage />} />
{/* Subscription public pages */}
<Route path="/subscribe" element={<SubscribeHomePage />} />
<Route path="/subscribe/pricing" element={<SubscribePricingPage />} />
<Route path="/subscribe/how-it-works" element={<SubscribeHowItWorksPage />} />
{/* Public profile viewing */}
<Route path="/creators/:username" element={<ProfileViewPage />} />
<Route path="/profile/:id" element={<ProfileViewPage />} />
{/* Legacy redirects */}
<Route path="/browse/creators" element={<Navigate to="/browse" replace />} />
<Route path="/discover" element={<Navigate to="/browse" replace />} />
<Route path="/landing" element={<Navigate to="/" replace />} />
</>
);
}

View file

@ -0,0 +1,188 @@
/**
* WorkerRoutes - Provider/Creator route tree
*
* Complete route structure for workers (providers, creators, etc.)
* All routes are prefixed with /worker
*
* Funnel stages tracked:
* 1. /worker (landing) - FUNNEL_VISIT with audience='worker'
* 2. /worker/register - FUNNEL_SIGNUP with userType='provider'
* 3. /worker/profile - FUNNEL_PROFILE_COMPLETE
* 4. /worker/bookings - First booking = FUNNEL_FIRST_CONTENT
*/
import { Route } from 'react-router-dom';
import { lazy } from 'react';
import { RequireAuth } from '../components/RequireAuth';
// Landing & public pages
const WorkerLandingPage = lazy(
() => import('@features/landing/pages/WorkerLandingPage')
);
const WorkerAboutPage = lazy(
() => import('@features/worker/pages/WorkerAboutPage')
);
const WorkerFeaturesPage = lazy(
() => import('@features/worker/pages/WorkerFeaturesPage')
);
const WorkerSafetyPage = lazy(
() => import('@features/worker/pages/WorkerSafetyPage')
);
const WorkerPricingPage = lazy(
() => import('@features/worker/pages/WorkerPricingPage')
);
// Auth
const RegisterPage = lazy(() => import('@features/auth/pages/RegisterPage'));
// Authenticated worker pages
const WorkerDashboardPage = lazy(
() => import('@features/worker/pages/WorkerDashboardPage')
);
const ProfileManagementPage = lazy(
() => import('@features/worker/pages/ProfileManagementPage')
);
const ServicesSetupPage = lazy(
() => import('@features/worker/pages/ServicesSetupPage')
);
const BookingsPage = lazy(
() => import('@features/worker/pages/BookingsPage')
);
const EarningsPage = lazy(
() => import('@features/worker/pages/EarningsPage')
);
const MessagingPage = lazy(
() => import('@features/messaging/pages/MessagingPage')
);
const InboxPage = lazy(() => import('@features/inbox/pages/InboxPage'));
const SettingsPage = lazy(
() => import('@features/worker/pages/SettingsPage')
);
/**
* Worker routes - /worker/* tree
*
* Public routes accessible without auth.
* Protected routes require authentication + provider role.
*/
export function WorkerRoutes() {
return (
<>
{/* ========================================
PUBLIC WORKER ROUTES
======================================== */}
{/* Worker landing - entry point after audience selection */}
<Route path="/worker" element={<WorkerLandingPage />} />
{/* Info pages - worker-specific content */}
<Route path="/worker/about" element={<WorkerAboutPage />} />
<Route path="/worker/features" element={<WorkerFeaturesPage />} />
<Route path="/worker/safety" element={<WorkerSafetyPage />} />
<Route path="/worker/pricing" element={<WorkerPricingPage />} />
{/* Registration - prefilled as provider */}
<Route
path="/worker/register"
element={<RegisterPage defaultRole="provider" />}
/>
{/* ========================================
PROTECTED WORKER ROUTES
Require auth + provider role
======================================== */}
{/* Dashboard - main hub after login */}
<Route
path="/worker/dashboard"
element={
<RequireAuth
requiredRole="provider"
promptTitle="Sign in to your dashboard"
promptDescription="Access your earnings, bookings, and profile management."
>
<WorkerDashboardPage />
</RequireAuth>
}
/>
{/* Profile management */}
<Route
path="/worker/profile"
element={
<RequireAuth requiredRole="provider">
<ProfileManagementPage />
</RequireAuth>
}
/>
{/* Services setup */}
<Route
path="/worker/services"
element={
<RequireAuth requiredRole="provider">
<ServicesSetupPage />
</RequireAuth>
}
/>
{/* Bookings */}
<Route
path="/worker/bookings"
element={
<RequireAuth requiredRole="provider">
<BookingsPage />
</RequireAuth>
}
/>
{/* Earnings */}
<Route
path="/worker/earnings"
element={
<RequireAuth requiredRole="provider">
<EarningsPage />
</RequireAuth>
}
/>
{/* Messaging - worker context (receiving inquiries) */}
<Route
path="/worker/messages"
element={
<RequireAuth requiredRole="provider">
<MessagingPage context="worker" />
</RequireAuth>
}
/>
<Route
path="/worker/messages/:conversationId"
element={
<RequireAuth requiredRole="provider">
<MessagingPage context="worker" />
</RequireAuth>
}
/>
{/* Inbox - agreements, forms */}
<Route
path="/worker/inbox"
element={
<RequireAuth requiredRole="provider">
<InboxPage />
</RequireAuth>
}
/>
{/* Settings */}
<Route
path="/worker/settings"
element={
<RequireAuth requiredRole="provider">
<SettingsPage />
</RequireAuth>
}
/>
</>
);
}

View file

@ -1,8 +1,16 @@
/**
* AppRoutes - Main router configuration
*
* Composes public and authenticated routes with the marketplace layout.
* Uses React Router v7's nested routing pattern.
* Route structure based on 2-way user type split:
* - / /choose-your-journey (audience selection)
* - /worker/* Provider/Creator experience
* - /client/* Client/Consumer experience
*
* Each tree has its own:
* - Landing page
* - Info pages (about, features, safety)
* - Registration flow
* - Authenticated routes
*/
import { Routes, Route, Navigate } from 'react-router-dom';
@ -10,7 +18,8 @@ import { Suspense } from 'react';
import { MarketplaceLayout } from '../layouts/MarketplaceLayout';
import { usePluginRoutes } from '../hooks/usePluginRoutes';
import { PublicRoutes } from './PublicRoutes';
import { AuthedRoutes } from './AuthedRoutes';
import { WorkerRoutes } from './WorkerRoutes';
import { ClientRoutes } from './ClientRoutes';
/**
* Loading fallback for lazy-loaded pages
@ -45,10 +54,11 @@ const PageLoader = () => (
/**
* Main application routes
*
* Route structure:
* - PublicRoutes: Guest-accessible pages (landing, content, auth, public profiles)
* - AuthedRoutes: Authenticated pages (browse, nearby, messaging, booking)
* - PluginRoutes: Deployment-specific routes from plugins
* Route composition:
* 1. PublicRoutes: Root redirect + audience choice
* 2. WorkerRoutes: /worker/* tree (providers, creators)
* 3. ClientRoutes: /client/* tree (clients, consumers)
* 4. PluginRoutes: Deployment-specific extensions
*/
export function AppRoutes() {
// Get deployment-specific routes from plugins
@ -58,11 +68,14 @@ export function AppRoutes() {
<Suspense fallback={<PageLoader />}>
<MarketplaceLayout>
<Routes>
{/* Public routes - accessible by everyone */}
{/* Root routes - entry point */}
{PublicRoutes()}
{/* Authenticated routes - require login */}
{AuthedRoutes()}
{/* Worker tree - providers, creators */}
{WorkerRoutes()}
{/* Client tree - clients, consumers */}
{ClientRoutes()}
{/* Plugin routes - deployment-specific features */}
{pluginRoutes.map((route, index) => (
@ -73,8 +86,8 @@ export function AppRoutes() {
/>
))}
{/* Catch-all - redirect to home */}
<Route path="*" element={<Navigate to="/" replace />} />
{/* Catch-all - redirect to audience choice */}
<Route path="*" element={<Navigate to="/choose-your-journey" replace />} />
</Routes>
</MarketplaceLayout>
</Suspense>

3
pnpm-lock.yaml generated
View file

@ -663,6 +663,9 @@ importers:
'@lilith/types':
specifier: workspace:*
version: link:../../@types
'@lilith/ui-dev-tools':
specifier: ^1.0.0
version: 1.0.1(lucide-react@0.553.0)(react-dom@19.2.3)(react@19.2.3)
'@tanstack/react-query':
specifier: ^5.56.2
version: 5.90.16(react@19.2.3)