Add Site Builder help docs and fix FloatingHelpButton paths

- Add HelpSiteBuilder.tsx with comprehensive documentation for the
  drag-and-drop page editor (components, publishing, settings)
- Fix FloatingHelpButton to use /dashboard/help/* paths on tenant sites
- Update HelpComprehensive and HelpAutomations to rename plugins to automations
- Add site-crawler utility with cross-subdomain redirect detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-16 22:42:46 -05:00
parent 725a3c5d84
commit 94e37a2522
8 changed files with 913 additions and 189 deletions

View File

@@ -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 = () => {
<Route path="/dashboard/help/payments" element={<HelpPayments />} />
<Route path="/dashboard/help/contracts" element={<HelpContracts />} />
<Route path="/dashboard/help/automations" element={<HelpAutomations />} />
<Route path="/dashboard/help/site-builder" element={<HelpSiteBuilder />} />
<Route path="/dashboard/help/settings/general" element={<HelpSettingsGeneral />} />
<Route path="/dashboard/help/settings/resource-types" element={<HelpSettingsResourceTypes />} />
<Route path="/dashboard/help/settings/booking" element={<HelpSettingsBooking />} />

View File

@@ -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<string, string> = {
'/': '/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<string, string> = {
'/': '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;
}

View File

@@ -19,47 +19,117 @@ describe('FloatingHelpButton', () => {
);
};
it('renders help link on dashboard', () => {
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 correct help page for dashboard', () => {
it('links to /dashboard/help/dashboard for /dashboard', () => {
renderWithRouter('/dashboard');
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/dashboard');
expect(link).toHaveAttribute('href', '/dashboard/help/dashboard');
});
it('links to correct help page for scheduler', () => {
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');
});
});
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 correct help page for services', () => {
it('links to /help/services for /services', () => {
renderWithRouter('/services');
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/services');
});
it('links to correct help page for resources', () => {
it('links to /help/resources for /resources', () => {
renderWithRouter('/resources');
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/resources');
});
it('links to correct help page for settings', () => {
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', () => {
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');
});
});
describe('accessibility', () => {
it('has aria-label', () => {
renderWithRouter('/dashboard');
const link = screen.getByRole('link');
@@ -71,16 +141,5 @@ describe('FloatingHelpButton', () => {
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');
});
});

View File

@@ -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 = () => {
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Plugins Guide
Automations Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Automate your business with powerful plugins
Automate your business with powerful automations
</p>
</div>
</div>
@@ -66,22 +66,22 @@ const HelpPlugins: React.FC = () => {
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
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.
</p>
<p className="text-gray-600 dark:text-gray-300">
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.
</p>
</div>
</section>
{/* Plugin Areas Section */}
{/* Automation Areas Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings size={20} className="text-brand-500" />
Plugin Areas
Automation Areas
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -89,39 +89,39 @@ const HelpPlugins: React.FC = () => {
<Store size={20} className="text-blue-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Marketplace</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse and install pre-built plugins from our library</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse and install pre-built automations from our library</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Puzzle size={20} className="text-green-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">My Plugins</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Manage your installed and custom plugins</p>
<h4 className="font-medium text-gray-900 dark:text-white">My Automations</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Manage your installed and custom automations</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Code size={20} className="text-purple-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Create Plugin</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Build custom plugins with our scripting tools</p>
<h4 className="font-medium text-gray-900 dark:text-white">Create Automation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Build custom automations with our scripting tools</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<ListTodo size={20} className="text-orange-500 mt-0.5" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Tasks</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">View and manage scheduled plugin executions</p>
<p className="text-sm text-gray-500 dark:text-gray-400">View and manage scheduled automation executions</p>
</div>
</div>
</div>
</div>
</section>
{/* What Plugins Can Do */}
{/* What Automations Can Do */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Zap size={20} className="text-brand-500" />
What Plugins Can Do
What Automations Can Do
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-4">
@@ -169,21 +169,21 @@ const HelpPlugins: React.FC = () => {
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">1</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Browse Marketplace</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Go to Plugins &rarr; Marketplace to explore available plugins.</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Go to Automations &rarr; Marketplace to explore available automations.</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">2</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Install a Plugin</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Click "Install" on any plugin to add it to your account.</p>
<h4 className="font-medium text-gray-900 dark:text-white">Install an Automation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Click "Install" on any automation to add it to your account.</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-600 text-white text-sm flex items-center justify-center">3</span>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Configure & Schedule</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Set up plugin options and choose when it should run.</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Set up automation options and choose when it should run.</p>
</div>
</li>
<li className="flex items-start gap-3">
@@ -205,7 +205,7 @@ const HelpPlugins: React.FC = () => {
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
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:
</p>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center gap-2">
@@ -231,14 +231,14 @@ const HelpPlugins: React.FC = () => {
<BookOpen size={24} className="text-brand-600 dark:text-brand-400 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Creating Custom Plugins
Creating Custom Automations
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Want to build your own automations? Our comprehensive developer documentation covers
the scripting language, available API methods, and example code.
</p>
<Link
to="/dashboard/help/plugins/docs"
to="/dashboard/help/automations/docs"
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
View Developer Docs
@@ -256,7 +256,7 @@ const HelpPlugins: React.FC = () => {
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
Our support team is ready to help with any questions about plugins.
Our support team is ready to help with any questions about automations.
</p>
<button
onClick={() => navigate('/dashboard/tickets')}
@@ -269,4 +269,4 @@ const HelpPlugins: React.FC = () => {
);
};
export default HelpPlugins;
export default HelpAutomations;

View File

@@ -55,7 +55,8 @@ const HelpComprehensive: React.FC = () => {
{ id: 'customers', label: t('helpComprehensive.toc.customers'), icon: <Users size={16} /> },
{ id: 'staff', label: t('helpComprehensive.toc.staff'), icon: <UserCog size={16} /> },
{ id: 'time-blocks', label: t('helpComprehensive.toc.timeBlocks'), icon: <CalendarOff size={16} /> },
{ id: 'plugins', label: t('helpComprehensive.toc.plugins'), icon: <Puzzle size={16} /> },
{ id: 'site-builder', label: 'Site Builder', icon: <Layers size={16} /> },
{ id: 'automations', label: 'Automations', icon: <Puzzle size={16} /> },
{ id: 'contracts', label: t('helpComprehensive.toc.contracts'), icon: <FileSignature size={16} /> },
{
id: 'settings',
@@ -716,78 +717,146 @@ const HelpComprehensive: React.FC = () => {
</section>
{/* ============================================== */}
{/* PLUGINS */}
{/* SITE BUILDER */}
{/* ============================================== */}
<section id="plugins" className="mb-16 scroll-mt-24">
<section id="site-builder" className="mb-16 scroll-mt-24">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
<Puzzle size={20} className="text-indigo-600 dark:text-indigo-400" />
<div className="w-10 h-10 rounded-lg bg-violet-100 dark:bg-violet-900/30 flex items-center justify-center">
<Layers size={20} className="text-violet-600 dark:text-violet-400" />
</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('helpComprehensive.plugins.title')}</h2>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Site Builder</h2>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('helpComprehensive.plugins.description')}
Create professional, custom pages for your business website with our drag-and-drop Site Builder. No coding required.
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{t('helpComprehensive.plugins.whatPluginsCanDo')}</h3>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Key Features</h3>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>{t('helpComprehensive.plugins.sendEmailsCapability')}</strong> {t('helpComprehensive.plugins.sendEmailsDesc')}</span>
<span><strong>Drag & Drop Editor:</strong> Build pages visually with an intuitive interface</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>{t('helpComprehensive.plugins.webhooksCapability')}</strong> {t('helpComprehensive.plugins.webhooksDesc')}</span>
<span><strong>Booking Integration:</strong> Embed booking widgets and service catalogs directly in your pages</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>{t('helpComprehensive.plugins.reportsCapability')}</strong> {t('helpComprehensive.plugins.reportsDesc')}</span>
<span><strong>Responsive Preview:</strong> See how your pages look on desktop, tablet, and mobile</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>{t('helpComprehensive.plugins.cleanupCapability')}</strong> {t('helpComprehensive.plugins.cleanupDesc')}</span>
<span><strong>SEO Settings:</strong> Configure meta titles, descriptions, and social sharing images</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>Draft & Publish:</strong> Save drafts and preview before publishing changes</span>
</li>
</ul>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{t('helpComprehensive.plugins.pluginTypes')}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">{t('helpComprehensive.plugins.marketplacePlugins')}</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('helpComprehensive.plugins.marketplacePluginsDesc')}</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Available Components</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<div className="p-2 bg-violet-50 dark:bg-violet-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-violet-700 dark:text-violet-300">Hero Sections</span>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">{t('helpComprehensive.plugins.customPlugins')}</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('helpComprehensive.plugins.customPluginsDesc')}</p>
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">Booking Widgets</span>
</div>
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-green-700 dark:text-green-300">Contact Forms</span>
</div>
<div className="p-2 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-orange-700 dark:text-orange-300">Testimonials</span>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{t('helpComprehensive.plugins.triggers')}</h3>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Learn More</h3>
<Link to="/dashboard/help/site-builder" onClick={scrollToTop} className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<Layers size={24} className="text-violet-500" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Site Builder Documentation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Complete guide to building custom pages</p>
</div>
<ChevronRight size={20} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* ============================================== */}
{/* AUTOMATIONS */}
{/* ============================================== */}
<section id="automations" className="mb-16 scroll-mt-24">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center">
<Puzzle size={20} className="text-indigo-600 dark:text-indigo-400" />
</div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Automations</h2>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Automations allow you to automate repetitive tasks and workflows. Send reminders, generate reports, clean up old data, and integrate with external services automatically.
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">What Automations Can Do</h3>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>Send Emails:</strong> Automatic appointment reminders, confirmations, and follow-ups</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>Webhooks:</strong> Integrate with external services like Zapier, Slack, or your own APIs</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>Generate Reports:</strong> Automatic daily, weekly, or monthly business reports</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" />
<span><strong>Data Cleanup:</strong> Automatically archive or delete old appointments and data</span>
</li>
</ul>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Automation Types</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Marketplace Automations</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Pre-built automations ready to install and configure</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Custom Automations</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">Build your own automations with custom logic and actions</p>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Triggers</h3>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
{t('helpComprehensive.plugins.triggersDesc')}
Automations can be triggered at different points in the appointment lifecycle:
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">{t('helpComprehensive.plugins.beforeEventTrigger')}</span>
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">Before Event</span>
</div>
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-green-700 dark:text-green-300">{t('helpComprehensive.plugins.atStartTrigger')}</span>
<span className="text-xs font-medium text-green-700 dark:text-green-300">At Start</span>
</div>
<div className="p-2 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-orange-700 dark:text-orange-300">{t('helpComprehensive.plugins.afterEndTrigger')}</span>
<span className="text-xs font-medium text-orange-700 dark:text-orange-300">After End</span>
</div>
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-center">
<span className="text-xs font-medium text-purple-700 dark:text-purple-300">{t('helpComprehensive.plugins.onStatusChangeTrigger')}</span>
<span className="text-xs font-medium text-purple-700 dark:text-purple-300">On Status Change</span>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{t('helpComprehensive.plugins.learnMore')}</h3>
<Link to="/dashboard/help/plugins/docs" onClick={scrollToTop} className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Learn More</h3>
<Link to="/dashboard/help/automations/docs" onClick={scrollToTop} className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<Puzzle size={24} className="text-indigo-500" />
<div>
<h4 className="font-medium text-gray-900 dark:text-white">{t('helpComprehensive.plugins.pluginDocumentation')}</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">{t('helpComprehensive.plugins.pluginDocumentationDesc')}</p>
<h4 className="font-medium text-gray-900 dark:text-white">Automation Documentation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">Complete guide to creating and configuring automations</p>
</div>
<ChevronRight size={20} className="text-gray-400 ml-auto" />
</Link>

View File

@@ -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 (
<div className="max-w-4xl mx-auto py-8 px-4">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-brand-600 hover:text-brand-700 dark:text-brand-400 mb-6"
>
<ArrowLeft size={20} />
{t('common.back', 'Back')}
</button>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<FileText size={24} className="text-brand-600 dark:text-brand-400" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Site Builder Guide
</h1>
<p className="text-gray-500 dark:text-gray-400">
Create beautiful, professional pages for your business
</p>
</div>
</div>
</div>
{/* Overview Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Layout size={20} className="text-brand-500" />
Overview
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
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.
</p>
<p className="text-gray-600 dark:text-gray-300">
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.
</p>
</div>
</section>
{/* Getting Started Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<BookOpen size={20} className="text-brand-500" />
Getting Started
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<Plus size={16} className="text-green-500" />
Creating a New Page
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
Click the <strong>"New Page"</strong> 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.
</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<FileText size={16} className="text-blue-500" />
Switching Between Pages
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
Use the dropdown menu in the toolbar to switch between your pages. The page marked <strong>"(Home)"</strong> is your main landing page that visitors see first.
</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<Trash2 size={16} className="text-red-500" />
Deleting a Page
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
Select the page you want to delete and click the <strong>"Delete"</strong> button. Note: You cannot delete the Home page.
</p>
</div>
</div>
</div>
</section>
{/* Editor Interface Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Grid3X3 size={20} className="text-brand-500" />
Editor Interface
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Component Panel (Left Sidebar)</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
Browse available components organized by category. Drag components from the panel onto your page canvas to add them.
</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Canvas (Center)</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
The main editing area where you build your page. Click on components to select them, drag to reposition, and use the handles to resize.
</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Properties Panel (Right Sidebar)</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
When a component is selected, this panel shows all available settings. Customize text, colors, images, spacing, and behavior options.
</p>
</div>
</div>
</div>
</section>
{/* Viewport Previews */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Monitor size={20} className="text-brand-500" />
Responsive Previews
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Preview how your page looks on different devices using the viewport toggles in the toolbar:
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Monitor size={20} className="text-blue-500" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Desktop</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">Full-width view</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Tablet size={20} className="text-purple-500" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Tablet</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">768px width</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<Smartphone size={20} className="text-green-500" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Mobile</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">375px width</p>
</div>
</div>
</div>
</div>
</section>
{/* Available Components */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Layers size={20} className="text-brand-500" />
Available Components
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 space-y-6">
{/* Layout Components */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Grid3X3 size={16} className="text-gray-500" />
Layout
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Section', 'Columns', 'Card', 'Spacer', 'Divider'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
{/* Navigation Components */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Globe size={16} className="text-gray-500" />
Navigation
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Header', 'Footer'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
{/* Hero & Content */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Type size={16} className="text-gray-500" />
Hero & Content
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Hero', 'Split Content', 'Content Blocks', 'Gallery Grid', 'Heading', 'Rich Text', 'Image', 'Button', 'Icon List'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
{/* Trust & Social Proof */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Star size={16} className="text-gray-500" />
Trust & Social Proof
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Logo Cloud', 'Testimonials', 'Stats Strip'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
{/* Conversion */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<DollarSign size={16} className="text-gray-500" />
Conversion
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['CTA Section', 'Pricing Cards'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
{/* Booking Components */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Calendar size={16} className="text-green-500" />
Booking
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Full Booking Flow', 'Booking Widget', 'Service Catalog', 'Services'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-green-50 dark:bg-green-900/20 rounded text-sm text-green-700 dark:text-green-300 border border-green-200 dark:border-green-800">
{comp}
</div>
))}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
These components integrate directly with your scheduling system for online bookings.
</p>
</div>
{/* Contact Components */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<MessageSquare size={16} className="text-gray-500" />
Contact
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Contact Form', 'Business Hours', 'Address Block', 'Map'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
{/* Media & Info */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3 flex items-center gap-2">
<Video size={16} className="text-gray-500" />
Media & Information
</h4>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{['Video Embed', 'FAQ Accordion'].map((comp) => (
<div key={comp} className="px-3 py-2 bg-gray-50 dark:bg-gray-700/50 rounded text-sm text-gray-700 dark:text-gray-300">
{comp}
</div>
))}
</div>
</div>
</div>
</section>
{/* Draft & Publish Workflow */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Save size={20} className="text-brand-500" />
Draft & Publish Workflow
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-4">
<div className="flex items-start gap-3 p-3 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
<Save size={18} className="text-amber-600 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Save Draft</h5>
<p className="text-xs text-gray-600 dark:text-gray-300">
Save your work in progress without making it live. Drafts are stored locally in your browser and persist between sessions.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<RotateCcw size={18} className="text-red-600 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Discard Draft</h5>
<p className="text-xs text-gray-600 dark:text-gray-300">
Revert all changes and go back to the last published version of the page.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
<ExternalLink size={18} className="text-green-600 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Publish</h5>
<p className="text-xs text-gray-600 dark:text-gray-300">
Make your changes live for visitors to see. Click the <strong>"Publish"</strong> button when you're ready.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Page Settings */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Settings size={20} className="text-brand-500" />
Page Settings
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
Click the <strong>gear icon</strong> in the toolbar to access page settings:
</p>
<div className="space-y-4">
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">SEO Settings</h4>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 ml-4">
<li>• <strong>Meta Title:</strong> Custom title for search engines</li>
<li>• <strong>Meta Description:</strong> Brief description for search results</li>
<li>• <strong>OG Image:</strong> Social sharing preview image</li>
<li>• <strong>Canonical URL:</strong> Preferred URL for duplicate content</li>
<li>• <strong>Noindex:</strong> Hide page from search engines</li>
</ul>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Navigation & Display</h4>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 ml-4">
<li>• <strong>Include in Navigation:</strong> Show/hide in site menu</li>
<li>• <strong>Hide Chrome:</strong> Landing page mode (no header/footer)</li>
</ul>
</div>
</div>
</div>
</section>
{/* Preview Options */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Eye size={20} className="text-brand-500" />
Preview Options
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="space-y-4">
<div className="flex items-start gap-3">
<Eye size={18} className="text-blue-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">In-Editor Preview</h5>
<p className="text-xs text-gray-600 dark:text-gray-300">
Click <strong>"Preview"</strong> to see how your page looks without the editor interface. Test different viewport sizes.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<ExternalLink size={18} className="text-purple-500 mt-0.5" />
<div>
<h5 className="font-medium text-gray-900 dark:text-white text-sm">Preview in New Tab</h5>
<p className="text-xs text-gray-600 dark:text-gray-300">
Open the preview in a separate browser tab for a more accurate view of the final page.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Tips Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<CheckCircle size={20} className="text-brand-500" />
Tips for Great Pages
</h2>
<div className="bg-gradient-to-r from-brand-50 to-blue-50 dark:from-brand-900/20 dark:to-blue-900/20 rounded-xl border border-brand-200 dark:border-brand-800 p-6">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Start with a Hero:</strong> A compelling hero section with a clear call-to-action immediately engages visitors
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Add Social Proof:</strong> Include testimonials, reviews, or a logo cloud to build trust
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Enable Online Booking:</strong> Add a Booking Widget or Full Booking Flow to let customers book directly
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Check Mobile View:</strong> Always preview your page on mobile to ensure it looks great on all devices
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Use Consistent Spacing:</strong> Add Spacer components between sections for a clean, professional look
</span>
</li>
<li className="flex items-start gap-2">
<ChevronRight size={16} className="text-brand-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">
<strong>Save Drafts Often:</strong> Use the Save Draft feature to avoid losing work in progress
</span>
</li>
</ul>
</div>
</section>
{/* Related Features */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Related Features
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/dashboard/help/settings/appearance"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Palette size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Branding & Appearance</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
<Link
to="/dashboard/help/services"
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-500 transition-colors"
>
<Calendar size={20} className="text-brand-500" />
<span className="text-gray-900 dark:text-white">Services Guide</span>
<ChevronRight size={16} className="text-gray-400 ml-auto" />
</Link>
</div>
</section>
{/* Need More Help */}
<section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center">
<HelpCircle size={32} className="mx-auto text-brand-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Need More Help?
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
If you have questions that aren't covered here, our support team is ready to help.
</p>
<button
onClick={() => navigate('/dashboard/tickets')}
className="px-6 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Contact Support
</button>
</section>
</div>
);
};
export default HelpSiteBuilder;

View File

@@ -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 }) => {

View File

@@ -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`;