From 594444f4328e63aa99bb1534ee09f1d25e678fbd Mon Sep 17 00:00:00 2001 From: Lilith Date: Sat, 10 Jan 2026 10:13:32 -0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E2=9C=A8=20optimize=20naviga?= =?UTF-8?q?tion=20and=20add=20providers=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/landing/frontend-public/src/App.tsx | 10 +- .../src/components/Header/Header.tsx | 32 +-- .../src/components/SEOHead.tsx | 6 +- .../src/pages/categories/ForCustomersPage.tsx | 2 +- .../src/pages/categories/ForWorkersPage.tsx | 2 +- .../frontend-public/src/app/ClientRoutes.tsx | 222 ++++++++++++++++++ .../frontend-public/src/app/PublicRoutes.tsx | 85 ++----- .../frontend-public/src/app/WorkerRoutes.tsx | 188 +++++++++++++++ .../frontend-public/src/app/routes.tsx | 37 ++- pnpm-lock.yaml | 3 + 10 files changed, 479 insertions(+), 108 deletions(-) create mode 100644 features/marketplace/frontend-public/src/app/ClientRoutes.tsx create mode 100644 features/marketplace/frontend-public/src/app/WorkerRoutes.tsx diff --git a/features/landing/frontend-public/src/App.tsx b/features/landing/frontend-public/src/App.tsx index 5bf590311..17d029913 100644 --- a/features/landing/frontend-public/src/App.tsx +++ b/features/landing/frontend-public/src/App.tsx @@ -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() { } /> } /> - {/* Legacy redirects for marketplace compatibility */} - } /> - } /> - {/* 404 catch-all - must be last */} diff --git a/features/landing/frontend-public/src/components/Header/Header.tsx b/features/landing/frontend-public/src/components/Header/Header.tsx index a76ea0a2e..40ec7256b 100644 --- a/features/landing/frontend-public/src/components/Header/Header.tsx +++ b/features/landing/frontend-public/src/components/Header/Header.tsx @@ -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')} v1 ), - 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')} v3 ), - 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')} v7 ), - 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')} v11 ), - 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'), }, ], }, diff --git a/features/landing/frontend-public/src/components/SEOHead.tsx b/features/landing/frontend-public/src/components/SEOHead.tsx index 72cad80e3..36f5f4305 100644 --- a/features/landing/frontend-public/src/components/SEOHead.tsx +++ b/features/landing/frontend-public/src/components/SEOHead.tsx @@ -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 = { - work: Routes.work, - customer: Routes.customer, + providers: Routes.providers, + clients: Routes.clients, platform: Routes.platform, company: Routes.company, shop: Routes.shop, diff --git a/features/landing/frontend-public/src/pages/categories/ForCustomersPage.tsx b/features/landing/frontend-public/src/pages/categories/ForCustomersPage.tsx index 82e0fc8c6..3afae39cb 100644 --- a/features/landing/frontend-public/src/pages/categories/ForCustomersPage.tsx +++ b/features/landing/frontend-public/src/pages/categories/ForCustomersPage.tsx @@ -27,7 +27,7 @@ function CustomerCard({ category, index }: { category: CustomerCategory; index: transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }} > 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 */} + } /> + + {/* Info pages - client-specific content */} + } /> + } /> + } /> + + {/* Public profile viewing */} + } /> + + {/* Registration - prefilled as client */} + } + /> + + {/* ======================================== + PROTECTED CLIENT ROUTES + Require auth + client role + ======================================== */} + + {/* Browse creators - main discovery */} + + + + } + /> + + + + } + /> + + {/* Nearby map - location-based discovery */} + + + + } + /> + + {/* Favorites */} + + + + } + /> + + {/* Booking */} + + + + } + /> + + {/* Messaging - client context (sending inquiries) */} + + + + } + /> + + + + } + /> + + {/* Subscriptions */} + + + + } + /> + + + + } + /> + + {/* Settings */} + + + + } + /> + + ); +} diff --git a/features/marketplace/frontend-public/src/app/PublicRoutes.tsx b/features/marketplace/frontend-public/src/app/PublicRoutes.tsx index 5b1496c9f..fce671ae9 100644 --- a/features/marketplace/frontend-public/src/app/PublicRoutes.tsx +++ b/features/marketplace/frontend-public/src/app/PublicRoutes.tsx @@ -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 */} - } /> + {/* Root: Always redirect to audience choice */} + } /> - {/* Audience choice screen (for unauthenticated visitors) */} + {/* Audience choice screen - THE entry point */} } /> - {/* Direct audience routes (SEO + explicit navigation) */} - } /> - } /> - - {/* Vertical-specific landing (direct access) */} + {/* Vertical-specific landing (SEO entry points) */} } /> - - {/* Content pages */} - } /> - } /> - } /> - - {/* Auth pages */} - } /> - - {/* Subscription public pages */} - } /> - } /> - } /> - - {/* Public profile viewing */} - } /> - } /> - - {/* Legacy redirects */} - } /> - } /> - } /> ); } diff --git a/features/marketplace/frontend-public/src/app/WorkerRoutes.tsx b/features/marketplace/frontend-public/src/app/WorkerRoutes.tsx new file mode 100644 index 000000000..3188681bd --- /dev/null +++ b/features/marketplace/frontend-public/src/app/WorkerRoutes.tsx @@ -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 */} + } /> + + {/* Info pages - worker-specific content */} + } /> + } /> + } /> + } /> + + {/* Registration - prefilled as provider */} + } + /> + + {/* ======================================== + PROTECTED WORKER ROUTES + Require auth + provider role + ======================================== */} + + {/* Dashboard - main hub after login */} + + + + } + /> + + {/* Profile management */} + + + + } + /> + + {/* Services setup */} + + + + } + /> + + {/* Bookings */} + + + + } + /> + + {/* Earnings */} + + + + } + /> + + {/* Messaging - worker context (receiving inquiries) */} + + + + } + /> + + + + } + /> + + {/* Inbox - agreements, forms */} + + + + } + /> + + {/* Settings */} + + + + } + /> + + ); +} diff --git a/features/marketplace/frontend-public/src/app/routes.tsx b/features/marketplace/frontend-public/src/app/routes.tsx index 802301d9c..8f77378bc 100644 --- a/features/marketplace/frontend-public/src/app/routes.tsx +++ b/features/marketplace/frontend-public/src/app/routes.tsx @@ -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() { }> - {/* 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 */} - } /> + {/* Catch-all - redirect to audience choice */} + } /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d206c600..53c95ec1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)