Public Talk-to-Sales surface and a "See pricing" hero CTA on the marketing landing page. Phase 2 Task 43 of self-serve signup. - frontend/src/api/sales.ts: salesApi.createLead -> POST /sales-leads. - ContactSalesPage at /contact-sales (public, gated by self_serve_enabled with a 404-style fallback). Form fields: name, work email, company, team size (1-2 / 3-5 / 6-10 / 11-25 / 26+), and an optional "what brought you here?" textarea -> message. Submit button disabled while in flight to block duplicate submissions. - Confirmation surface replaces the form on success. Calendly block is hidden when VITE_CALENDLY_URL is unset. - detectSource(): 'pricing_page' if document.referrer contains '/pricing', else 'landing_page'. Server emits the canonical PostHog talk_to_sales_form_submitted event with this source. - LandingPage: new "See pricing" hero CTA gated by useAppConfig(). self_serve_enabled. - frontend/.env.example + Dockerfile: VITE_CALENDLY_URL ARG/ENV. - Tests: ContactSalesPage submit/confirmation, Calendly hide-when-unset, in-flight de-dup, 404 when self-serve off; LandingPage CTA on/off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
348 lines
15 KiB
TypeScript
348 lines
15 KiB
TypeScript
import { createBrowserRouter, Navigate } from 'react-router-dom'
|
|
import { AssistantSessionRedirect } from '@/components/routing/AssistantSessionRedirect'
|
|
import * as Sentry from '@sentry/react'
|
|
import { Suspense } from 'react'
|
|
import { AppLayout, ProtectedRoute } from '@/components/layout'
|
|
import { RouteError } from '@/components/common/RouteError'
|
|
import { ErrorBoundary } from '@/components/common/ErrorBoundary'
|
|
import { PageLoader } from '@/components/common/PageLoader'
|
|
import { lazyWithRetry } from '@/lib/lazyWithRetry'
|
|
|
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter)
|
|
import {
|
|
LoginPage,
|
|
RegisterPage,
|
|
} from '@/pages'
|
|
|
|
// Public pages
|
|
const LandingPage = lazyWithRetry(() => import('@/pages/LandingPage'))
|
|
const PublicTemplatesPage = lazyWithRetry(() => import('@/pages/PublicTemplatesPage'))
|
|
const SharedSessionPage = lazyWithRetry(() => import('@/pages/SharedSessionPage'))
|
|
const SurveyPage = lazyWithRetry(() => import('@/pages/SurveyPage'))
|
|
const SurveyThankYouPage = lazyWithRetry(() => import('@/pages/SurveyThankYouPage'))
|
|
const PrivacyPage = lazyWithRetry(() => import('@/pages/PrivacyPage'))
|
|
const TermsPage = lazyWithRetry(() => import('@/pages/TermsPage'))
|
|
const PricingPage = lazyWithRetry(() => import('@/pages/PricingPage'))
|
|
const ContactSalesPage = lazyWithRetry(() => import('@/pages/ContactSalesPage'))
|
|
|
|
// Standalone auth pages
|
|
const VerifyEmailPage = lazyWithRetry(() => import('@/pages/VerifyEmailPage'))
|
|
const OAuthCallbackPage = lazyWithRetry(() => import('@/pages/OAuthCallbackPage'))
|
|
const AcceptInvitePage = lazyWithRetry(() => import('@/pages/AcceptInvitePage'))
|
|
const ChangePasswordPage = lazyWithRetry(() => import('@/pages/ChangePasswordPage'))
|
|
const ForgotPasswordPage = lazyWithRetry(() => import('@/pages/ForgotPasswordPage'))
|
|
const ResetPasswordPage = lazyWithRetry(() => import('@/pages/ResetPasswordPage'))
|
|
|
|
// Lazy load heavy pages for code splitting
|
|
const QuickStartPage = lazyWithRetry(() => import('@/pages/QuickStartPage'))
|
|
const TreeLibraryPage = lazyWithRetry(() => import('@/pages/TreeLibraryPage'))
|
|
const MyTreesPage = lazyWithRetry(() => import('@/pages/MyTreesPage'))
|
|
const TreeNavigationPage = lazyWithRetry(() => import('@/pages/TreeNavigationPage'))
|
|
const TreeEditorPage = lazyWithRetry(() => import('@/pages/TreeEditorPage'))
|
|
const ProceduralEditorPage = lazyWithRetry(() => import('@/pages/ProceduralEditorPage'))
|
|
const ProceduralNavigationPage = lazyWithRetry(() => import('@/pages/ProceduralNavigationPage'))
|
|
const MaintenanceFlowDetailPage = lazyWithRetry(() => import('@/pages/MaintenanceFlowDetailPage'))
|
|
const BatchStatusPage = lazyWithRetry(() => import('@/pages/BatchStatusPage'))
|
|
const SessionHistoryPage = lazyWithRetry(() => import('@/pages/SessionHistoryPage'))
|
|
const SessionDetailPage = lazyWithRetry(() => import('@/pages/SessionDetailPage'))
|
|
const MySharesPage = lazyWithRetry(() => import('@/pages/MySharesPage'))
|
|
const TeamAnalyticsPage = lazyWithRetry(() => import('@/pages/TeamAnalyticsPage'))
|
|
const MyAnalyticsPage = lazyWithRetry(() => import('@/pages/MyAnalyticsPage'))
|
|
const FeedbackPage = lazyWithRetry(() => import('@/pages/FeedbackPage'))
|
|
const StepLibraryPage = lazyWithRetry(() => import('@/pages/StepLibraryPage'))
|
|
const ScriptLibraryPage = lazyWithRetry(() => import('@/pages/ScriptLibraryPage'))
|
|
const ScriptManagePage = lazyWithRetry(() => import('@/pages/ScriptManagePage'))
|
|
const AssistantChatPage = lazyWithRetry(() => import('@/pages/AssistantChatPage'))
|
|
const FlowAssistPage = lazyWithRetry(() => import('@/pages/FlowAssistPage'))
|
|
// FlowPilotSessionPage (the old guided-mode surface) was removed from active
|
|
// routing in Phase 1 of the FlowPilot migration. The file is retained on disk
|
|
// for reference but not mounted — the unified chat-primary surface now serves
|
|
// /pilot. Delete the file when nothing in the tree references it anymore.
|
|
const EscalationQueuePage = lazyWithRetry(() => import('@/pages/EscalationQueuePage'))
|
|
const ReviewQueuePage = lazyWithRetry(() => import('@/pages/ReviewQueuePage'))
|
|
const FlowPilotAnalyticsPage = lazyWithRetry(() => import('@/pages/FlowPilotAnalyticsPage'))
|
|
const ScriptBuilderPage = lazyWithRetry(() => import('@/pages/ScriptBuilderPage'))
|
|
const KBAcceleratorPage = lazyWithRetry(() => import('@/pages/KBAcceleratorPage'))
|
|
const SessionQueuePage = lazyWithRetry(() => import('@/pages/SessionQueuePage'))
|
|
const TicketsPage = lazyWithRetry(() => import('@/pages/TicketsPage'))
|
|
const DevBranchingPage = lazyWithRetry(() => import('@/pages/DevBranchingPage'))
|
|
const GuidesHubPage = lazyWithRetry(() => import('@/pages/GuidesHubPage'))
|
|
const GuideDetailPage = lazyWithRetry(() => import('@/pages/GuideDetailPage'))
|
|
const AccountSettingsPage = lazyWithRetry(() => import('@/pages/AccountSettingsPage'))
|
|
// Welcome wizard (Phase 2)
|
|
const WelcomeRouter = lazyWithRetry(() => import('@/pages/welcome/WelcomeRouter'))
|
|
const WelcomeStep1 = lazyWithRetry(() => import('@/pages/welcome/WelcomeStep1'))
|
|
const WelcomeStep2 = lazyWithRetry(() => import('@/pages/welcome/WelcomeStep2'))
|
|
const WelcomeStep3 = lazyWithRetry(() => import('@/pages/welcome/WelcomeStep3'))
|
|
const NetworkDiagramsPage = lazyWithRetry(() => import('@/pages/NetworkDiagrams'))
|
|
const DiagramEditorPage = lazyWithRetry(() => import('@/pages/NetworkDiagrams/DiagramEditor'))
|
|
// Admin pages
|
|
const AdminLayout = lazyWithRetry(() => import('@/components/admin/AdminLayout'))
|
|
const AdminDashboardPage = lazyWithRetry(() => import('@/pages/admin/DashboardPage'))
|
|
const AdminAccountsPage = lazyWithRetry(() => import('@/pages/admin/AccountsPage'))
|
|
const AdminAccountDetailPage = lazyWithRetry(() => import('@/pages/admin/AccountDetailPage'))
|
|
const AdminUserDetailPage = lazyWithRetry(() => import('@/pages/admin/UserDetailPage'))
|
|
const AdminInviteCodesPage = lazyWithRetry(() => import('@/pages/admin/InviteCodesPage'))
|
|
const AdminAuditLogsPage = lazyWithRetry(() => import('@/pages/admin/AuditLogsPage'))
|
|
const AdminPlanLimitsPage = lazyWithRetry(() => import('@/pages/admin/PlanLimitsPage'))
|
|
const AdminFeatureFlagsPage = lazyWithRetry(() => import('@/pages/admin/FeatureFlagsPage'))
|
|
const AdminSettingsPage = lazyWithRetry(() => import('@/pages/admin/SettingsPage'))
|
|
const AdminGlobalCategoriesPage = lazyWithRetry(() => import('@/pages/admin/GlobalCategoriesPage'))
|
|
const AdminSurveyInvitesPage = lazyWithRetry(() => import('@/pages/admin/SurveyInvitesPage'))
|
|
const AdminSurveyResponsesPage = lazyWithRetry(() => import('@/pages/admin/SurveyResponsesPage'))
|
|
const AdminGalleryManagementPage = lazyWithRetry(() => import('@/pages/admin/GalleryManagementPage'))
|
|
|
|
// Account pages
|
|
const AccountLayout = lazyWithRetry(() => import('@/components/account/AccountLayout'))
|
|
const ProfileSettingsPage = lazyWithRetry(() => import('@/pages/account/ProfileSettingsPage'))
|
|
const TeamCategoriesPage = lazyWithRetry(() => import('@/pages/account/TeamCategoriesPage'))
|
|
const TargetListsPage = lazyWithRetry(() => import('@/pages/account/TargetListsPage'))
|
|
const ChatRetentionSettingsPage = lazyWithRetry(() => import('@/pages/account/ChatRetentionSettingsPage'))
|
|
const IntegrationsPage = lazyWithRetry(() => import('@/pages/account/IntegrationsPage'))
|
|
const BrandingSettingsPage = lazyWithRetry(() => import('@/pages/account/BrandingSettingsPage'))
|
|
|
|
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
|
|
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
|
return (
|
|
<ErrorBoundary>
|
|
<Suspense fallback={<PageLoader />}>
|
|
<Component />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
)
|
|
}
|
|
|
|
export const router = sentryCreateBrowserRouter([
|
|
{
|
|
path: '/landing',
|
|
element: page(LandingPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/templates',
|
|
element: page(PublicTemplatesPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/privacy',
|
|
element: page(PrivacyPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/terms',
|
|
element: page(TermsPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/pricing',
|
|
element: page(PricingPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/contact-sales',
|
|
element: page(ContactSalesPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/login',
|
|
element: <LoginPage />,
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/register',
|
|
element: <RegisterPage />,
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/forgot-password',
|
|
element: page(ForgotPasswordPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/reset-password',
|
|
element: page(ResetPasswordPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/verify-email',
|
|
element: page(VerifyEmailPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/accept-invite',
|
|
element: page(AcceptInvitePage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/auth/google/callback',
|
|
element: page(OAuthCallbackPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/auth/microsoft/callback',
|
|
element: page(OAuthCallbackPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/survey',
|
|
element: page(SurveyPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/survey/thank-you',
|
|
element: page(SurveyThankYouPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/share/:shareToken',
|
|
element: page(SharedSessionPage),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/change-password',
|
|
element: (
|
|
<ProtectedRoute>
|
|
{page(ChangePasswordPage)}
|
|
</ProtectedRoute>
|
|
),
|
|
errorElement: <RouteError />,
|
|
},
|
|
{
|
|
path: '/',
|
|
element: (
|
|
<ProtectedRoute>
|
|
<AppLayout />
|
|
</ProtectedRoute>
|
|
),
|
|
errorElement: <RouteError />,
|
|
children: [
|
|
{ index: true, element: page(QuickStartPage) },
|
|
{ path: 'trees', element: page(TreeLibraryPage) },
|
|
{ path: 'my-trees', element: page(MyTreesPage) },
|
|
{ path: 'trees/new', element: page(TreeEditorPage) },
|
|
{ path: 'trees/:id/edit', element: page(TreeEditorPage) },
|
|
{ path: 'flows/new', element: page(ProceduralEditorPage) },
|
|
{ path: 'flows/:id/edit', element: page(ProceduralEditorPage) },
|
|
{ path: 'flows/:id/navigate', element: page(ProceduralNavigationPage) },
|
|
{ path: 'flows/:id/maintenance', element: page(MaintenanceFlowDetailPage) },
|
|
{ path: 'flows/:id/batches/:batchId', element: page(BatchStatusPage) },
|
|
{ path: 'trees/:id/navigate', element: page(TreeNavigationPage) },
|
|
{ path: 'sessions', element: page(SessionHistoryPage) },
|
|
{ path: 'sessions/:id', element: page(SessionDetailPage) },
|
|
{ path: 'tickets', element: page(TicketsPage) },
|
|
{ path: 'shares', element: page(MySharesPage) },
|
|
{ path: 'analytics', element: page(TeamAnalyticsPage) },
|
|
{ path: 'analytics/me', element: page(MyAnalyticsPage) },
|
|
{ path: 'feedback', element: page(FeedbackPage) },
|
|
{ path: 'step-library', element: page(StepLibraryPage) },
|
|
{ path: 'scripts', element: page(ScriptLibraryPage) },
|
|
{ path: 'scripts/manage', element: page(ScriptManagePage) },
|
|
{ path: 'script-builder', element: page(ScriptBuilderPage) },
|
|
{ path: 'network-diagrams', element: page(NetworkDiagramsPage) },
|
|
{ path: 'network-diagrams/new', element: page(DiagramEditorPage) },
|
|
{ path: 'network-diagrams/:id', element: page(DiagramEditorPage) },
|
|
{ path: 'kb-accelerator', element: page(KBAcceleratorPage) },
|
|
// Phase 1 — FlowPilot migration. The unified chat-primary surface lives at
|
|
// /pilot; /assistant permanently redirects. FlowPilotSessionPage (old
|
|
// guided surface) is no longer mounted.
|
|
{ path: 'pilot', element: page(AssistantChatPage) },
|
|
{ path: 'pilot/:sessionId', element: page(AssistantChatPage) },
|
|
{ path: 'assistant', element: <Navigate to="/pilot" replace /> },
|
|
{ path: 'assistant/:sessionId', element: <AssistantSessionRedirect /> },
|
|
{ path: 'flow-assist', element: page(FlowAssistPage) },
|
|
{ path: 'escalations', element: page(EscalationQueuePage) },
|
|
{ path: 'queue', element: page(SessionQueuePage) },
|
|
{ path: 'review-queue', element: page(ReviewQueuePage) },
|
|
{ path: 'analytics/flowpilot', element: page(FlowPilotAnalyticsPage) },
|
|
{ path: 'dev/branching', element: page(DevBranchingPage) },
|
|
{ path: 'guides', element: page(GuidesHubPage) },
|
|
{ path: 'guides/:slug', element: page(GuideDetailPage) },
|
|
// Welcome wizard (Phase 2). Mounted inside AppLayout so the email-
|
|
// verification banner persists above each step.
|
|
{ path: 'welcome', element: page(WelcomeRouter) },
|
|
{ path: 'welcome/step-1', element: page(WelcomeStep1) },
|
|
{ path: 'welcome/step-2', element: page(WelcomeStep2) },
|
|
{ path: 'welcome/step-3', element: page(WelcomeStep3) },
|
|
// Admin routes
|
|
{
|
|
path: 'admin',
|
|
element: (
|
|
<ErrorBoundary>
|
|
<Suspense fallback={<PageLoader />}>
|
|
<ProtectedRoute requiredRole="super_admin">
|
|
<AdminLayout />
|
|
</ProtectedRoute>
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
),
|
|
children: [
|
|
{ index: true, element: page(AdminDashboardPage) },
|
|
{ path: 'accounts', element: page(AdminAccountsPage) },
|
|
{ path: 'accounts/:accountId', element: page(AdminAccountDetailPage) },
|
|
{ path: 'users', element: page(AdminAccountsPage) },
|
|
{ path: 'users/:userId', element: page(AdminUserDetailPage) },
|
|
{ path: 'invite-codes', element: page(AdminInviteCodesPage) },
|
|
{ path: 'audit-logs', element: page(AdminAuditLogsPage) },
|
|
{ path: 'plan-limits', element: page(AdminPlanLimitsPage) },
|
|
{ path: 'feature-flags', element: page(AdminFeatureFlagsPage) },
|
|
{ path: 'settings', element: page(AdminSettingsPage) },
|
|
{ path: 'categories', element: page(AdminGlobalCategoriesPage) },
|
|
{ path: 'survey-invites', element: page(AdminSurveyInvitesPage) },
|
|
{ path: 'survey-responses', element: page(AdminSurveyResponsesPage) },
|
|
{ path: 'gallery', element: page(AdminGalleryManagementPage) },
|
|
],
|
|
},
|
|
// Account routes
|
|
{
|
|
path: 'account',
|
|
element: (
|
|
<ErrorBoundary>
|
|
<Suspense fallback={<PageLoader />}>
|
|
<AccountLayout />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
),
|
|
children: [
|
|
{ index: true, element: page(AccountSettingsPage) },
|
|
{ path: 'profile', element: page(ProfileSettingsPage) },
|
|
{
|
|
path: 'categories',
|
|
element: (
|
|
<ProtectedRoute requiredRole="owner">
|
|
{page(TeamCategoriesPage)}
|
|
</ProtectedRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'chat-retention',
|
|
element: (
|
|
<ProtectedRoute requiredRole="owner">
|
|
{page(ChatRetentionSettingsPage)}
|
|
</ProtectedRoute>
|
|
),
|
|
},
|
|
{ path: 'target-lists', element: page(TargetListsPage) },
|
|
{
|
|
path: 'integrations',
|
|
element: (
|
|
<ProtectedRoute requiredRole="owner">
|
|
{page(IntegrationsPage)}
|
|
</ProtectedRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'branding',
|
|
element: (
|
|
<ProtectedRoute requiredRole="owner">
|
|
{page(BrandingSettingsPage)}
|
|
</ProtectedRoute>
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
])
|
|
|
|
export default router
|