diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3c5e5046..bfdd3679 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -91,6 +91,7 @@ const HelpMessages = React.lazy(() => import('./pages/help/HelpMessages')); const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments')); const HelpContracts = React.lazy(() => import('./pages/help/HelpContracts')); const HelpAutomations = React.lazy(() => import('./pages/help/HelpAutomations')); +const HelpSiteBuilder = React.lazy(() => import('./pages/help/HelpSiteBuilder')); const HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral')); const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes')); const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking')); @@ -760,6 +761,7 @@ const AppContent: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/FloatingHelpButton.tsx b/frontend/src/components/FloatingHelpButton.tsx index c1a7e680..576490d3 100644 --- a/frontend/src/components/FloatingHelpButton.tsx +++ b/frontend/src/components/FloatingHelpButton.tsx @@ -10,76 +10,85 @@ import { Link, useLocation } from 'react-router-dom'; import { HelpCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -// Map routes to their help paths -const routeToHelpPath: Record = { - '/': '/help/dashboard', - '/dashboard': '/help/dashboard', - '/scheduler': '/help/scheduler', - '/tasks': '/help/tasks', - '/customers': '/help/customers', - '/services': '/help/services', - '/resources': '/help/resources', - '/staff': '/help/staff', - '/time-blocks': '/help/time-blocks', - '/my-availability': '/help/time-blocks', - '/messages': '/help/messages', - '/tickets': '/help/ticketing', - '/payments': '/help/payments', - '/contracts': '/help/contracts', - '/contracts/templates': '/help/contracts', - '/automations': '/help/automations', - '/automations/marketplace': '/help/automations', - '/automations/my-automations': '/help/automations', - '/automations/create': '/help/automations/docs', - '/settings': '/help/settings/general', - '/settings/general': '/help/settings/general', - '/settings/resource-types': '/help/settings/resource-types', - '/settings/booking': '/help/settings/booking', - '/settings/appearance': '/help/settings/appearance', - '/settings/email': '/help/settings/email', - '/settings/domains': '/help/settings/domains', - '/settings/api': '/help/settings/api', - '/settings/auth': '/help/settings/auth', - '/settings/billing': '/help/settings/billing', - '/settings/quota': '/help/settings/quota', - // Platform routes - '/platform/dashboard': '/help/dashboard', - '/platform/businesses': '/help/dashboard', - '/platform/users': '/help/staff', - '/platform/tickets': '/help/ticketing', +// Map route suffixes to their help page suffixes +// These get prefixed appropriately based on context (tenant dashboard or public) +const routeToHelpSuffix: Record = { + '/': 'dashboard', + '/dashboard': 'dashboard', + '/scheduler': 'scheduler', + '/tasks': 'tasks', + '/customers': 'customers', + '/services': 'services', + '/resources': 'resources', + '/staff': 'staff', + '/time-blocks': 'time-blocks', + '/my-availability': 'time-blocks', + '/messages': 'messages', + '/tickets': 'ticketing', + '/payments': 'payments', + '/contracts': 'contracts', + '/contracts/templates': 'contracts', + '/automations': 'automations', + '/automations/marketplace': 'automations', + '/automations/my-automations': 'automations', + '/automations/create': 'automations/docs', + '/site-editor': 'site-builder', + '/gallery': 'site-builder', + '/settings': 'settings/general', + '/settings/general': 'settings/general', + '/settings/resource-types': 'settings/resource-types', + '/settings/booking': 'settings/booking', + '/settings/appearance': 'settings/appearance', + '/settings/email': 'settings/email', + '/settings/domains': 'settings/domains', + '/settings/api': 'settings/api', + '/settings/auth': 'settings/auth', + '/settings/billing': 'settings/billing', + '/settings/quota': 'settings/quota', }; const FloatingHelpButton: React.FC = () => { const { t } = useTranslation(); const location = useLocation(); + // Check if we're on a tenant dashboard route + const isOnDashboard = location.pathname.startsWith('/dashboard'); + // Get the help path for the current route const getHelpPath = (): string => { + // Determine the base help path based on context + const helpBase = isOnDashboard ? '/dashboard/help' : '/help'; + + // Get the route to look up (strip /dashboard prefix if present) + const lookupPath = isOnDashboard + ? location.pathname.replace(/^\/dashboard/, '') || '/' + : location.pathname; + // Exact match first - if (routeToHelpPath[location.pathname]) { - return routeToHelpPath[location.pathname]; + if (routeToHelpSuffix[lookupPath]) { + return `${helpBase}/${routeToHelpSuffix[lookupPath]}`; } // Try matching with a prefix (for dynamic routes like /customers/:id) - const pathSegments = location.pathname.split('/').filter(Boolean); + const pathSegments = lookupPath.split('/').filter(Boolean); if (pathSegments.length > 0) { // Try progressively shorter paths for (let i = pathSegments.length; i > 0; i--) { const testPath = '/' + pathSegments.slice(0, i).join('/'); - if (routeToHelpPath[testPath]) { - return routeToHelpPath[testPath]; + if (routeToHelpSuffix[testPath]) { + return `${helpBase}/${routeToHelpSuffix[testPath]}`; } } } - // Default to the main help guide - return '/help'; + // Default to the main help page + return helpBase; }; const helpPath = getHelpPath(); // Don't show on help pages themselves - if (location.pathname.startsWith('/help')) { + if (location.pathname.includes('/help')) { return null; } diff --git a/frontend/src/components/__tests__/FloatingHelpButton.test.tsx b/frontend/src/components/__tests__/FloatingHelpButton.test.tsx index 91d5a470..02d67ff6 100644 --- a/frontend/src/components/__tests__/FloatingHelpButton.test.tsx +++ b/frontend/src/components/__tests__/FloatingHelpButton.test.tsx @@ -19,68 +19,127 @@ describe('FloatingHelpButton', () => { ); }; - it('renders help link on dashboard', () => { - renderWithRouter('/dashboard'); - const link = screen.getByRole('link'); - expect(link).toBeInTheDocument(); + describe('tenant dashboard routes (prefixed with /dashboard)', () => { + it('renders help link on tenant dashboard', () => { + renderWithRouter('/dashboard'); + const link = screen.getByRole('link'); + expect(link).toBeInTheDocument(); + }); + + it('links to /dashboard/help/dashboard for /dashboard', () => { + renderWithRouter('/dashboard'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/dashboard'); + }); + + it('links to /dashboard/help/scheduler for /dashboard/scheduler', () => { + renderWithRouter('/dashboard/scheduler'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/scheduler'); + }); + + it('links to /dashboard/help/services for /dashboard/services', () => { + renderWithRouter('/dashboard/services'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/services'); + }); + + it('links to /dashboard/help/resources for /dashboard/resources', () => { + renderWithRouter('/dashboard/resources'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/resources'); + }); + + it('links to /dashboard/help/settings/general for /dashboard/settings/general', () => { + renderWithRouter('/dashboard/settings/general'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/settings/general'); + }); + + it('links to /dashboard/help/customers for /dashboard/customers/123', () => { + renderWithRouter('/dashboard/customers/123'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/customers'); + }); + + it('returns null on /dashboard/help pages', () => { + const { container } = renderWithRouter('/dashboard/help/dashboard'); + expect(container.firstChild).toBeNull(); + }); + + it('links to /dashboard/help for unknown dashboard routes', () => { + renderWithRouter('/dashboard/unknown-route'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help'); + }); + + it('links to /dashboard/help/site-builder for /dashboard/site-editor', () => { + renderWithRouter('/dashboard/site-editor'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/site-builder'); + }); + + it('links to /dashboard/help/site-builder for /dashboard/gallery', () => { + renderWithRouter('/dashboard/gallery'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard/help/site-builder'); + }); }); - it('links to correct help page for dashboard', () => { - renderWithRouter('/dashboard'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help/dashboard'); + describe('non-dashboard routes (public/platform)', () => { + it('links to /help/scheduler for /scheduler', () => { + renderWithRouter('/scheduler'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/help/scheduler'); + }); + + it('links to /help/services for /services', () => { + renderWithRouter('/services'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/help/services'); + }); + + it('links to /help/resources for /resources', () => { + renderWithRouter('/resources'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/help/resources'); + }); + + it('links to /help/settings/general for /settings/general', () => { + renderWithRouter('/settings/general'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/help/settings/general'); + }); + + it('returns null on /help pages', () => { + const { container } = renderWithRouter('/help/dashboard'); + expect(container.firstChild).toBeNull(); + }); + + it('links to /help for unknown routes', () => { + renderWithRouter('/unknown-route'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/help'); + }); + + it('handles dynamic routes by matching prefix', () => { + renderWithRouter('/customers/123'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/help/customers'); + }); }); - it('links to correct help page for scheduler', () => { - renderWithRouter('/scheduler'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help/scheduler'); - }); + describe('accessibility', () => { + it('has aria-label', () => { + renderWithRouter('/dashboard'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('aria-label', 'Help'); + }); - it('links to correct help page for services', () => { - renderWithRouter('/services'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help/services'); - }); - - it('links to correct help page for resources', () => { - renderWithRouter('/resources'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help/resources'); - }); - - it('links to correct help page for settings', () => { - renderWithRouter('/settings/general'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help/settings/general'); - }); - - it('returns null on help pages', () => { - const { container } = renderWithRouter('/help/dashboard'); - expect(container.firstChild).toBeNull(); - }); - - it('has aria-label', () => { - renderWithRouter('/dashboard'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('aria-label', 'Help'); - }); - - it('has title attribute', () => { - renderWithRouter('/dashboard'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('title', 'Help'); - }); - - it('links to default help for unknown routes', () => { - renderWithRouter('/unknown-route'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help'); - }); - - it('handles dynamic routes by matching prefix', () => { - renderWithRouter('/customers/123'); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/help/customers'); + it('has title attribute', () => { + renderWithRouter('/dashboard'); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('title', 'Help'); + }); }); }); diff --git a/frontend/src/pages/help/HelpAutomations.tsx b/frontend/src/pages/help/HelpAutomations.tsx index 4e131223..39848a77 100644 --- a/frontend/src/pages/help/HelpAutomations.tsx +++ b/frontend/src/pages/help/HelpAutomations.tsx @@ -1,7 +1,7 @@ /** - * Help Plugins Page + * Help Automations Page * - * User-friendly help documentation for Plugins. + * User-friendly help documentation for Automations. */ import React from 'react'; @@ -26,7 +26,7 @@ import { ListTodo, } from 'lucide-react'; -const HelpPlugins: React.FC = () => { +const HelpAutomations: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -49,10 +49,10 @@ const HelpPlugins: React.FC = () => {

- Plugins Guide + Automations Guide

- Automate your business with powerful plugins + Automate your business with powerful automations

@@ -66,22 +66,22 @@ const HelpPlugins: React.FC = () => {

- Plugins extend your scheduling platform with automation capabilities. Send reminder emails, + Automations extend your scheduling platform with powerful automation capabilities. Send reminder emails, generate reports, track no-shows, and create custom workflows - all running automatically on schedules you define.

- Browse the marketplace for ready-made plugins, or create your own custom automations + Browse the marketplace for ready-made automations, or create your own custom automations using our simple scripting language.

- {/* Plugin Areas Section */} + {/* Automation Areas Section */}

- Plugin Areas + Automation Areas

@@ -89,39 +89,39 @@ const HelpPlugins: React.FC = () => {

Marketplace

-

Browse and install pre-built plugins from our library

+

Browse and install pre-built automations from our library

-

My Plugins

-

Manage your installed and custom plugins

+

My Automations

+

Manage your installed and custom automations

-

Create Plugin

-

Build custom plugins with our scripting tools

+

Create Automation

+

Build custom automations with our scripting tools

Tasks

-

View and manage scheduled plugin executions

+

View and manage scheduled automation executions

- {/* What Plugins Can Do */} + {/* What Automations Can Do */}

- What Plugins Can Do + What Automations Can Do

@@ -169,21 +169,21 @@ const HelpPlugins: React.FC = () => { 1

Browse Marketplace

-

Go to Plugins → Marketplace to explore available plugins.

+

Go to Automations → Marketplace to explore available automations.

  • 2
    -

    Install a Plugin

    -

    Click "Install" on any plugin to add it to your account.

    +

    Install an Automation

    +

    Click "Install" on any automation to add it to your account.

  • 3

    Configure & Schedule

    -

    Set up plugin options and choose when it should run.

    +

    Set up automation options and choose when it should run.

  • @@ -205,7 +205,7 @@ const HelpPlugins: React.FC = () => {

    - When you install a plugin, it creates a scheduled task that runs automatically. Use the Tasks page to: + When you install an automation, it creates a scheduled task that runs automatically. Use the Tasks page to:

    • @@ -231,14 +231,14 @@ const HelpPlugins: React.FC = () => {

      - Creating Custom Plugins + Creating Custom Automations

      Want to build your own automations? Our comprehensive developer documentation covers the scripting language, available API methods, and example code.

      View Developer Docs @@ -256,7 +256,7 @@ const HelpPlugins: React.FC = () => { Need More Help?

      - Our support team is ready to help with any questions about plugins. + Our support team is ready to help with any questions about automations.

  • {/* ============================================== */} - {/* PLUGINS */} + {/* SITE BUILDER */} {/* ============================================== */} -
    +
    -
    - +
    +
    -

    {t('helpComprehensive.plugins.title')}

    +

    Site Builder

    - {t('helpComprehensive.plugins.description')} + Create professional, custom pages for your business website with our drag-and-drop Site Builder. No coding required.

    -

    {t('helpComprehensive.plugins.whatPluginsCanDo')}

    +

    Key Features

    • - {t('helpComprehensive.plugins.sendEmailsCapability')} {t('helpComprehensive.plugins.sendEmailsDesc')} + Drag & Drop Editor: Build pages visually with an intuitive interface
    • - {t('helpComprehensive.plugins.webhooksCapability')} {t('helpComprehensive.plugins.webhooksDesc')} + Booking Integration: Embed booking widgets and service catalogs directly in your pages
    • - {t('helpComprehensive.plugins.reportsCapability')} {t('helpComprehensive.plugins.reportsDesc')} + Responsive Preview: See how your pages look on desktop, tablet, and mobile
    • - {t('helpComprehensive.plugins.cleanupCapability')} {t('helpComprehensive.plugins.cleanupDesc')} + SEO Settings: Configure meta titles, descriptions, and social sharing images +
    • +
    • + + Draft & Publish: Save drafts and preview before publishing changes
    -

    {t('helpComprehensive.plugins.pluginTypes')}

    -
    -
    -

    {t('helpComprehensive.plugins.marketplacePlugins')}

    -

    {t('helpComprehensive.plugins.marketplacePluginsDesc')}

    +

    Available Components

    +
    +
    + Hero Sections
    -
    -

    {t('helpComprehensive.plugins.customPlugins')}

    -

    {t('helpComprehensive.plugins.customPluginsDesc')}

    +
    + Booking Widgets +
    +
    + Contact Forms +
    +
    + Testimonials
    -

    {t('helpComprehensive.plugins.triggers')}

    +

    Learn More

    + + +
    +

    Site Builder Documentation

    +

    Complete guide to building custom pages

    +
    + + +
    +
    + + {/* ============================================== */} + {/* AUTOMATIONS */} + {/* ============================================== */} +
    +
    +
    + +
    +

    Automations

    +
    + +
    +

    + Automations allow you to automate repetitive tasks and workflows. Send reminders, generate reports, clean up old data, and integrate with external services automatically. +

    + +

    What Automations Can Do

    +
      +
    • + + Send Emails: Automatic appointment reminders, confirmations, and follow-ups +
    • +
    • + + Webhooks: Integrate with external services like Zapier, Slack, or your own APIs +
    • +
    • + + Generate Reports: Automatic daily, weekly, or monthly business reports +
    • +
    • + + Data Cleanup: Automatically archive or delete old appointments and data +
    • +
    + +

    Automation Types

    +
    +
    +

    Marketplace Automations

    +

    Pre-built automations ready to install and configure

    +
    +
    +

    Custom Automations

    +

    Build your own automations with custom logic and actions

    +
    +
    + +

    Triggers

    - {t('helpComprehensive.plugins.triggersDesc')} + Automations can be triggered at different points in the appointment lifecycle:

    - {t('helpComprehensive.plugins.beforeEventTrigger')} + Before Event
    - {t('helpComprehensive.plugins.atStartTrigger')} + At Start
    - {t('helpComprehensive.plugins.afterEndTrigger')} + After End
    - {t('helpComprehensive.plugins.onStatusChangeTrigger')} + On Status Change
    -

    {t('helpComprehensive.plugins.learnMore')}

    - +

    Learn More

    +
    -

    {t('helpComprehensive.plugins.pluginDocumentation')}

    -

    {t('helpComprehensive.plugins.pluginDocumentationDesc')}

    +

    Automation Documentation

    +

    Complete guide to creating and configuring automations

    diff --git a/frontend/src/pages/help/HelpSiteBuilder.tsx b/frontend/src/pages/help/HelpSiteBuilder.tsx new file mode 100644 index 00000000..0345d58c --- /dev/null +++ b/frontend/src/pages/help/HelpSiteBuilder.tsx @@ -0,0 +1,526 @@ +/** + * Help Site Builder Page + * + * Comprehensive help documentation for the Site Builder (Page Editor). + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, Link } from 'react-router-dom'; +import { + ArrowLeft, + FileText, + Layout, + Plus, + Trash2, + Monitor, + Tablet, + Smartphone, + Settings, + Eye, + Save, + RotateCcw, + ExternalLink, + CheckCircle, + ChevronRight, + HelpCircle, + Layers, + Type, + Image, + MousePointer, + Grid3X3, + Palette, + Search, + Globe, + BookOpen, + Calendar, + Users, + MessageSquare, + Map, + Clock, + Star, + Video, + DollarSign, + HelpCircle as FAQ, +} from 'lucide-react'; + +const HelpSiteBuilder: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + return ( +
    + {/* Back Button */} + + + {/* Header */} +
    +
    +
    + +
    +
    +

    + Site Builder Guide +

    +

    + Create beautiful, professional pages for your business +

    +
    +
    +
    + + {/* Overview Section */} +
    +

    + + Overview +

    +
    +

    + The Site Builder is a powerful drag-and-drop page editor that lets you create custom pages for your business website. Build landing pages, service pages, about pages, and more without any coding knowledge. +

    +

    + Your pages are automatically branded with your business colors and can include booking widgets, service catalogs, contact forms, and other interactive elements that connect with your scheduling system. +

    +
    +
    + + {/* Getting Started Section */} +
    +

    + + Getting Started +

    +
    +
    +
    +

    + + Creating a New Page +

    +

    + Click the "New Page" button in the top toolbar and enter a title for your page. The page will be created with a blank canvas ready for you to add components. +

    +
    +
    +

    + + Switching Between Pages +

    +

    + Use the dropdown menu in the toolbar to switch between your pages. The page marked "(Home)" is your main landing page that visitors see first. +

    +
    +
    +

    + + Deleting a Page +

    +

    + Select the page you want to delete and click the "Delete" button. Note: You cannot delete the Home page. +

    +
    +
    +
    +
    + + {/* Editor Interface Section */} +
    +

    + + Editor Interface +

    +
    +
    +
    +

    Component Panel (Left Sidebar)

    +

    + Browse available components organized by category. Drag components from the panel onto your page canvas to add them. +

    +
    +
    +

    Canvas (Center)

    +

    + The main editing area where you build your page. Click on components to select them, drag to reposition, and use the handles to resize. +

    +
    +
    +

    Properties Panel (Right Sidebar)

    +

    + When a component is selected, this panel shows all available settings. Customize text, colors, images, spacing, and behavior options. +

    +
    +
    +
    +
    + + {/* Viewport Previews */} +
    +

    + + Responsive Previews +

    +
    +

    + Preview how your page looks on different devices using the viewport toggles in the toolbar: +

    +
    +
    + +
    +
    Desktop
    +

    Full-width view

    +
    +
    +
    + +
    +
    Tablet
    +

    768px width

    +
    +
    +
    + +
    +
    Mobile
    +

    375px width

    +
    +
    +
    +
    +
    + + {/* Available Components */} +
    +

    + + Available Components +

    +
    + {/* Layout Components */} +
    +

    + + Layout +

    +
    + {['Section', 'Columns', 'Card', 'Spacer', 'Divider'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    + + {/* Navigation Components */} +
    +

    + + Navigation +

    +
    + {['Header', 'Footer'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    + + {/* Hero & Content */} +
    +

    + + Hero & Content +

    +
    + {['Hero', 'Split Content', 'Content Blocks', 'Gallery Grid', 'Heading', 'Rich Text', 'Image', 'Button', 'Icon List'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    + + {/* Trust & Social Proof */} +
    +

    + + Trust & Social Proof +

    +
    + {['Logo Cloud', 'Testimonials', 'Stats Strip'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    + + {/* Conversion */} +
    +

    + + Conversion +

    +
    + {['CTA Section', 'Pricing Cards'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    + + {/* Booking Components */} +
    +

    + + Booking +

    +
    + {['Full Booking Flow', 'Booking Widget', 'Service Catalog', 'Services'].map((comp) => ( +
    + {comp} +
    + ))} +
    +

    + These components integrate directly with your scheduling system for online bookings. +

    +
    + + {/* Contact Components */} +
    +

    + + Contact +

    +
    + {['Contact Form', 'Business Hours', 'Address Block', 'Map'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    + + {/* Media & Info */} +
    +

    +

    +
    + {['Video Embed', 'FAQ Accordion'].map((comp) => ( +
    + {comp} +
    + ))} +
    +
    +
    +
    + + {/* Draft & Publish Workflow */} +
    +

    + + Draft & Publish Workflow +

    +
    +
    +
    + +
    +
    Save Draft
    +

    + Save your work in progress without making it live. Drafts are stored locally in your browser and persist between sessions. +

    +
    +
    +
    + +
    +
    Discard Draft
    +

    + Revert all changes and go back to the last published version of the page. +

    +
    +
    +
    + +
    +
    Publish
    +

    + Make your changes live for visitors to see. Click the "Publish" button when you're ready. +

    +
    +
    +
    +
    +
    + + {/* Page Settings */} +
    +

    + + Page Settings +

    +
    +

    + Click the gear icon in the toolbar to access page settings: +

    +
    +
    +

    SEO Settings

    +
      +
    • Meta Title: Custom title for search engines
    • +
    • Meta Description: Brief description for search results
    • +
    • OG Image: Social sharing preview image
    • +
    • Canonical URL: Preferred URL for duplicate content
    • +
    • Noindex: Hide page from search engines
    • +
    +
    +
    +

    Navigation & Display

    +
      +
    • Include in Navigation: Show/hide in site menu
    • +
    • Hide Chrome: Landing page mode (no header/footer)
    • +
    +
    +
    +
    +
    + + {/* Preview Options */} +
    +

    + + Preview Options +

    +
    +
    +
    + +
    +
    In-Editor Preview
    +

    + Click "Preview" to see how your page looks without the editor interface. Test different viewport sizes. +

    +
    +
    +
    + +
    +
    Preview in New Tab
    +

    + Open the preview in a separate browser tab for a more accurate view of the final page. +

    +
    +
    +
    +
    +
    + + {/* Tips Section */} +
    +

    + + Tips for Great Pages +

    +
    +
      +
    • + + + Start with a Hero: A compelling hero section with a clear call-to-action immediately engages visitors + +
    • +
    • + + + Add Social Proof: Include testimonials, reviews, or a logo cloud to build trust + +
    • +
    • + + + Enable Online Booking: Add a Booking Widget or Full Booking Flow to let customers book directly + +
    • +
    • + + + Check Mobile View: Always preview your page on mobile to ensure it looks great on all devices + +
    • +
    • + + + Use Consistent Spacing: Add Spacer components between sections for a clean, professional look + +
    • +
    • + + + Save Drafts Often: Use the Save Draft feature to avoid losing work in progress + +
    • +
    +
    +
    + + {/* Related Features */} +
    +

    + Related Features +

    +
    + + + Branding & Appearance + + + + + Services Guide + + +
    +
    + + {/* Need More Help */} +
    + +

    + Need More Help? +

    +

    + If you have questions that aren't covered here, our support team is ready to help. +

    + +
    +
    + ); +}; + +export default HelpSiteBuilder; diff --git a/frontend/tests/e2e/site-crawler.spec.ts b/frontend/tests/e2e/site-crawler.spec.ts index e0d1ef18..9b04afd9 100644 --- a/frontend/tests/e2e/site-crawler.spec.ts +++ b/frontend/tests/e2e/site-crawler.spec.ts @@ -117,33 +117,47 @@ test.describe('Site Crawler - Platform Dashboard', () => { }); }); -// Uncomment and configure when tenant test users are set up -// test.describe('Site Crawler - Tenant Dashboard', () => { -// test('should crawl tenant dashboard without errors', async ({ page, context }) => { -// const user = TEST_USERS.businessOwner; -// const loggedIn = await loginAsUser(page, user); -// expect(loggedIn).toBe(true); -// -// const crawler = new SiteCrawler(page, context, CRAWLER_OPTIONS); -// const report = await crawler.crawl('http://demo.lvh.me:5173'); -// -// console.log(formatReport(report)); -// -// expect(report.totalPages).toBeGreaterThan(0); -// -// const criticalErrors = report.results.flatMap(r => -// r.errors.filter(e => { -// if (e.message.includes('React DevTools')) return false; -// if (e.message.includes('favicon.ico')) return false; -// if (e.message.includes('hot-update')) return false; -// if (e.message.includes('WebSocket')) return false; -// return true; -// }) -// ); -// -// expect(criticalErrors.length).toBe(0); -// }); -// }); +test.describe('Site Crawler - Tenant Dashboard', () => { + test('should crawl tenant dashboard without cross-subdomain redirects', async ({ page, context }) => { + const user = TEST_USERS.businessOwner; + const loggedIn = await loginAsUser(page, user); + expect(loggedIn).toBe(true); + + const crawler = new SiteCrawler(page, context, { + ...CRAWLER_OPTIONS, + detectCrossSubdomainRedirects: true, // Detect redirects to public site + }); + const report = await crawler.crawl('http://demo.lvh.me:5173'); + + console.log(formatReport(report)); + + expect(report.totalPages).toBeGreaterThan(0); + + // Filter critical errors (excluding minor dev warnings) + const criticalErrors = report.results.flatMap(r => + r.errors.filter(e => { + if (e.message.includes('React DevTools')) return false; + if (e.message.includes('favicon.ico')) return false; + if (e.message.includes('hot-update')) return false; + if (e.message.includes('WebSocket')) return false; + return true; + }) + ); + + // Log redirect issues separately for visibility + const redirectErrors = criticalErrors.filter(e => e.type === 'redirect'); + if (redirectErrors.length > 0) { + console.error('\n⚠️ CROSS-SUBDOMAIN REDIRECTS DETECTED:'); + redirectErrors.forEach(e => { + console.error(` - ${e.url}`); + console.error(` ${e.message}`); + if (e.details) console.error(` ${e.details}`); + }); + } + + expect(criticalErrors.length).toBe(0); + }); +}); // test.describe('Site Crawler - Customer Portal', () => { // test('should crawl customer booking portal without errors', async ({ page, context }) => { diff --git a/frontend/tests/e2e/utils/crawler.ts b/frontend/tests/e2e/utils/crawler.ts index 634a0ad8..bf3a7587 100644 --- a/frontend/tests/e2e/utils/crawler.ts +++ b/frontend/tests/e2e/utils/crawler.ts @@ -7,7 +7,7 @@ import { Page, BrowserContext } from '@playwright/test'; export interface CrawlError { url: string; - type: 'console' | 'network' | 'broken-link' | 'page-error'; + type: 'console' | 'network' | 'broken-link' | 'page-error' | 'redirect'; message: string; details?: string; timestamp: Date; @@ -33,6 +33,7 @@ export interface CrawlReport { networkErrors: number; brokenLinks: number; pageErrors: number; + redirects: number; }; } @@ -45,6 +46,8 @@ export interface CrawlerOptions { screenshotOnError?: boolean; screenshotDir?: string; verbose?: boolean; + /** Detect when page redirects to a different subdomain (e.g., tenant to public) */ + detectCrossSubdomainRedirects?: boolean; } const DEFAULT_OPTIONS: CrawlerOptions = { @@ -63,6 +66,7 @@ const DEFAULT_OPTIONS: CrawlerOptions = { screenshotOnError: false, screenshotDir: 'test-results/crawler-screenshots', verbose: false, + detectCrossSubdomainRedirects: false, }; export class SiteCrawler { @@ -114,6 +118,24 @@ export class SiteCrawler { } } + private getSubdomain(url: string): string | null { + try { + const parsed = new URL(url); + const hostname = parsed.hostname; + // Extract subdomain from *.lvh.me + if (hostname.endsWith('.lvh.me')) { + const parts = hostname.split('.'); + if (parts.length >= 3) { + return parts[0]; + } + } + // No subdomain (e.g., lvh.me itself) + return null; + } catch { + return null; + } + } + private shouldCrawl(url: string): boolean { // Skip if already visited if (this.visited.has(url)) { @@ -280,6 +302,23 @@ export class SiteCrawler { }); } + // Check for cross-subdomain redirects + if (this.options.detectCrossSubdomainRedirects) { + const finalUrl = this.page.url(); + const requestedSubdomain = this.getSubdomain(url); + const finalSubdomain = this.getSubdomain(finalUrl); + + if (requestedSubdomain && requestedSubdomain !== finalSubdomain) { + errors.push({ + url, + type: 'redirect', + message: `Redirected from ${requestedSubdomain}.lvh.me to ${finalSubdomain || 'root'}.lvh.me`, + details: `Final URL: ${finalUrl}`, + timestamp: new Date(), + }); + } + } + // Wait a bit for React to render and any async operations await this.page.waitForTimeout(500); @@ -378,6 +417,7 @@ export class SiteCrawler { networkErrors: 0, brokenLinks: 0, pageErrors: 0, + redirects: 0, }; for (const result of this.results) { @@ -395,6 +435,9 @@ export class SiteCrawler { case 'page-error': summary.pageErrors++; break; + case 'redirect': + summary.redirects++; + break; } } } @@ -427,7 +470,8 @@ export function formatReport(report: CrawlReport): string { output += ` Console errors: ${report.summary.consoleErrors}\n`; output += ` Network errors: ${report.summary.networkErrors}\n`; output += ` Broken links: ${report.summary.brokenLinks}\n`; - output += ` Page errors: ${report.summary.pageErrors}\n\n`; + output += ` Page errors: ${report.summary.pageErrors}\n`; + output += ` Redirects: ${report.summary.redirects}\n\n`; // List pages with errors const pagesWithErrors = report.results.filter(r => r.errors.length > 0); @@ -443,7 +487,8 @@ export function formatReport(report: CrawlReport): string { for (const error of result.errors) { const icon = error.type === 'console' ? '⚠️' : error.type === 'network' ? '🌐' : - error.type === 'broken-link' ? '🔴' : '💥'; + error.type === 'broken-link' ? '🔴' : + error.type === 'redirect' ? '↪️' : '💥'; output += ` ${icon} [${error.type.toUpperCase()}] ${error.message}\n`; if (error.details) { output += ` Details: ${error.details.substring(0, 200)}\n`;