+
+ 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 */}
+
navigate(-1)}
+ className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
+ >
+
+ {t('common.back', 'Back')}
+
+
+ {/* 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
+
+
+
+
+
+
+
+
+ {/* 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 */}
+
+
+
+ Media & Information
+
+
+ {['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.
+
+ navigate('/dashboard/tickets')}
+ className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
+ >
+ Contact Support
+
+
+
+ );
+};
+
+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`;