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 HelpPayments = React.lazy(() => import('./pages/help/HelpPayments'));
const HelpContracts = React.lazy(() => import('./pages/help/HelpContracts')); const HelpContracts = React.lazy(() => import('./pages/help/HelpContracts'));
const HelpAutomations = React.lazy(() => import('./pages/help/HelpAutomations')); 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 HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral'));
const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes')); const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes'));
const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking')); 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/payments" element={<HelpPayments />} />
<Route path="/dashboard/help/contracts" element={<HelpContracts />} /> <Route path="/dashboard/help/contracts" element={<HelpContracts />} />
<Route path="/dashboard/help/automations" element={<HelpAutomations />} /> <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/general" element={<HelpSettingsGeneral />} />
<Route path="/dashboard/help/settings/resource-types" element={<HelpSettingsResourceTypes />} /> <Route path="/dashboard/help/settings/resource-types" element={<HelpSettingsResourceTypes />} />
<Route path="/dashboard/help/settings/booking" element={<HelpSettingsBooking />} /> <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 { HelpCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// Map routes to their help paths // Map route suffixes to their help page suffixes
const routeToHelpPath: Record<string, string> = { // These get prefixed appropriately based on context (tenant dashboard or public)
'/': '/help/dashboard', const routeToHelpSuffix: Record<string, string> = {
'/dashboard': '/help/dashboard', '/': 'dashboard',
'/scheduler': '/help/scheduler', '/dashboard': 'dashboard',
'/tasks': '/help/tasks', '/scheduler': 'scheduler',
'/customers': '/help/customers', '/tasks': 'tasks',
'/services': '/help/services', '/customers': 'customers',
'/resources': '/help/resources', '/services': 'services',
'/staff': '/help/staff', '/resources': 'resources',
'/time-blocks': '/help/time-blocks', '/staff': 'staff',
'/my-availability': '/help/time-blocks', '/time-blocks': 'time-blocks',
'/messages': '/help/messages', '/my-availability': 'time-blocks',
'/tickets': '/help/ticketing', '/messages': 'messages',
'/payments': '/help/payments', '/tickets': 'ticketing',
'/contracts': '/help/contracts', '/payments': 'payments',
'/contracts/templates': '/help/contracts', '/contracts': 'contracts',
'/automations': '/help/automations', '/contracts/templates': 'contracts',
'/automations/marketplace': '/help/automations', '/automations': 'automations',
'/automations/my-automations': '/help/automations', '/automations/marketplace': 'automations',
'/automations/create': '/help/automations/docs', '/automations/my-automations': 'automations',
'/settings': '/help/settings/general', '/automations/create': 'automations/docs',
'/settings/general': '/help/settings/general', '/site-editor': 'site-builder',
'/settings/resource-types': '/help/settings/resource-types', '/gallery': 'site-builder',
'/settings/booking': '/help/settings/booking', '/settings': 'settings/general',
'/settings/appearance': '/help/settings/appearance', '/settings/general': 'settings/general',
'/settings/email': '/help/settings/email', '/settings/resource-types': 'settings/resource-types',
'/settings/domains': '/help/settings/domains', '/settings/booking': 'settings/booking',
'/settings/api': '/help/settings/api', '/settings/appearance': 'settings/appearance',
'/settings/auth': '/help/settings/auth', '/settings/email': 'settings/email',
'/settings/billing': '/help/settings/billing', '/settings/domains': 'settings/domains',
'/settings/quota': '/help/settings/quota', '/settings/api': 'settings/api',
// Platform routes '/settings/auth': 'settings/auth',
'/platform/dashboard': '/help/dashboard', '/settings/billing': 'settings/billing',
'/platform/businesses': '/help/dashboard', '/settings/quota': 'settings/quota',
'/platform/users': '/help/staff',
'/platform/tickets': '/help/ticketing',
}; };
const FloatingHelpButton: React.FC = () => { const FloatingHelpButton: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); 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 // Get the help path for the current route
const getHelpPath = (): string => { 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 // Exact match first
if (routeToHelpPath[location.pathname]) { if (routeToHelpSuffix[lookupPath]) {
return routeToHelpPath[location.pathname]; return `${helpBase}/${routeToHelpSuffix[lookupPath]}`;
} }
// Try matching with a prefix (for dynamic routes like /customers/:id) // 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) { if (pathSegments.length > 0) {
// Try progressively shorter paths // Try progressively shorter paths
for (let i = pathSegments.length; i > 0; i--) { for (let i = pathSegments.length; i > 0; i--) {
const testPath = '/' + pathSegments.slice(0, i).join('/'); const testPath = '/' + pathSegments.slice(0, i).join('/');
if (routeToHelpPath[testPath]) { if (routeToHelpSuffix[testPath]) {
return routeToHelpPath[testPath]; return `${helpBase}/${routeToHelpSuffix[testPath]}`;
} }
} }
} }
// Default to the main help guide // Default to the main help page
return '/help'; return helpBase;
}; };
const helpPath = getHelpPath(); const helpPath = getHelpPath();
// Don't show on help pages themselves // Don't show on help pages themselves
if (location.pathname.startsWith('/help')) { if (location.pathname.includes('/help')) {
return null; 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'); renderWithRouter('/dashboard');
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
}); });
it('links to correct help page for dashboard', () => { it('links to /dashboard/help/dashboard for /dashboard', () => {
renderWithRouter('/dashboard'); renderWithRouter('/dashboard');
const link = screen.getByRole('link'); 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'); renderWithRouter('/scheduler');
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/scheduler'); expect(link).toHaveAttribute('href', '/help/scheduler');
}); });
it('links to correct help page for services', () => { it('links to /help/services for /services', () => {
renderWithRouter('/services'); renderWithRouter('/services');
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/services'); expect(link).toHaveAttribute('href', '/help/services');
}); });
it('links to correct help page for resources', () => { it('links to /help/resources for /resources', () => {
renderWithRouter('/resources'); renderWithRouter('/resources');
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/resources'); 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'); renderWithRouter('/settings/general');
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/help/settings/general'); expect(link).toHaveAttribute('href', '/help/settings/general');
}); });
it('returns null on help pages', () => { it('returns null on /help pages', () => {
const { container } = renderWithRouter('/help/dashboard'); const { container } = renderWithRouter('/help/dashboard');
expect(container.firstChild).toBeNull(); 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', () => { it('has aria-label', () => {
renderWithRouter('/dashboard'); renderWithRouter('/dashboard');
const link = screen.getByRole('link'); const link = screen.getByRole('link');
@@ -71,16 +141,5 @@ describe('FloatingHelpButton', () => {
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('title', 'Help'); 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'; import React from 'react';
@@ -26,7 +26,7 @@ import {
ListTodo, ListTodo,
} from 'lucide-react'; } from 'lucide-react';
const HelpPlugins: React.FC = () => { const HelpAutomations: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -49,10 +49,10 @@ const HelpPlugins: React.FC = () => {
</div> </div>
<div> <div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Plugins Guide Automations Guide
</h1> </h1>
<p className="text-gray-500 dark:text-gray-400"> <p className="text-gray-500 dark:text-gray-400">
Automate your business with powerful plugins Automate your business with powerful automations
</p> </p>
</div> </div>
</div> </div>
@@ -66,22 +66,22 @@ const HelpPlugins: React.FC = () => {
</h2> </h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6"> <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"> <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 generate reports, track no-shows, and create custom workflows - all running automatically
on schedules you define. on schedules you define.
</p> </p>
<p className="text-gray-600 dark:text-gray-300"> <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. using our simple scripting language.
</p> </p>
</div> </div>
</section> </section>
{/* Plugin Areas Section */} {/* Automation Areas Section */}
<section className="mb-10"> <section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> <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" /> <Settings size={20} className="text-brand-500" />
Plugin Areas Automation Areas
</h2> </h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6"> <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"> <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" /> <Store size={20} className="text-blue-500 mt-0.5" />
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-white">Marketplace</h4> <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> </div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> <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" /> <Puzzle size={20} className="text-green-500 mt-0.5" />
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-white">My Plugins</h4> <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 plugins</p> <p className="text-sm text-gray-500 dark:text-gray-400">Manage your installed and custom automations</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> <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" /> <Code size={20} className="text-purple-500 mt-0.5" />
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-white">Create Plugin</h4> <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 plugins with our scripting tools</p> <p className="text-sm text-gray-500 dark:text-gray-400">Build custom automations with our scripting tools</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> <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" /> <ListTodo size={20} className="text-orange-500 mt-0.5" />
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-white">Tasks</h4> <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>
</div> </div>
</div> </div>
</section> </section>
{/* What Plugins Can Do */} {/* What Automations Can Do */}
<section className="mb-10"> <section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> <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" /> <Zap size={20} className="text-brand-500" />
What Plugins Can Do What Automations Can Do
</h2> </h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6"> <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="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> <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> <div>
<h4 className="font-medium text-gray-900 dark:text-white">Browse Marketplace</h4> <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> </div>
</li> </li>
<li className="flex items-start gap-3"> <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> <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> <div>
<h4 className="font-medium text-gray-900 dark:text-white">Install a Plugin</h4> <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 plugin to add it to your account.</p> <p className="text-sm text-gray-500 dark:text-gray-400">Click "Install" on any automation to add it to your account.</p>
</div> </div>
</li> </li>
<li className="flex items-start gap-3"> <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> <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> <div>
<h4 className="font-medium text-gray-900 dark:text-white">Configure & Schedule</h4> <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> </div>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
@@ -205,7 +205,7 @@ const HelpPlugins: React.FC = () => {
</h2> </h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6"> <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"> <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> </p>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300"> <ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex items-center gap-2"> <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" /> <BookOpen size={24} className="text-brand-600 dark:text-brand-400 mt-1" />
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Creating Custom Plugins Creating Custom Automations
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
Want to build your own automations? Our comprehensive developer documentation covers Want to build your own automations? Our comprehensive developer documentation covers
the scripting language, available API methods, and example code. the scripting language, available API methods, and example code.
</p> </p>
<Link <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" 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 View Developer Docs
@@ -256,7 +256,7 @@ const HelpPlugins: React.FC = () => {
Need More Help? Need More Help?
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300 mb-4"> <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> </p>
<button <button
onClick={() => navigate('/dashboard/tickets')} 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: 'customers', label: t('helpComprehensive.toc.customers'), icon: <Users size={16} /> },
{ id: 'staff', label: t('helpComprehensive.toc.staff'), icon: <UserCog 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: '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: 'contracts', label: t('helpComprehensive.toc.contracts'), icon: <FileSignature size={16} /> },
{ {
id: 'settings', id: 'settings',
@@ -716,78 +717,146 @@ const HelpComprehensive: React.FC = () => {
</section> </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="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"> <div className="w-10 h-10 rounded-lg bg-violet-100 dark:bg-violet-900/30 flex items-center justify-center">
<Puzzle size={20} className="text-indigo-600 dark:text-indigo-400" /> <Layers size={20} className="text-violet-600 dark:text-violet-400" />
</div> </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>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 mb-6"> <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"> <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> </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"> <ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300 mb-6">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" /> <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>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" /> <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>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" /> <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>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<CheckCircle size={16} className="text-green-500" /> <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> </li>
</ul> </ul>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{t('helpComprehensive.plugins.pluginTypes')}</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Available Components</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg border border-indigo-200 dark:border-indigo-800"> <div className="p-2 bg-violet-50 dark:bg-violet-900/20 rounded-lg text-center">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">{t('helpComprehensive.plugins.marketplacePlugins')}</h4> <span className="text-xs font-medium text-violet-700 dark:text-violet-300">Hero Sections</span>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('helpComprehensive.plugins.marketplacePluginsDesc')}</p>
</div> </div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800"> <div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
<h4 className="font-medium text-gray-900 dark:text-white mb-1">{t('helpComprehensive.plugins.customPlugins')}</h4> <span className="text-xs font-medium text-blue-700 dark:text-blue-300">Booking Widgets</span>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('helpComprehensive.plugins.customPluginsDesc')}</p> </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>
</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"> <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> </p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6"> <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"> <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>
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg text-center"> <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>
<div className="p-2 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center"> <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>
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-center"> <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>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">{t('helpComprehensive.plugins.learnMore')}</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">Learn More</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"> <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" /> <Puzzle size={24} className="text-indigo-500" />
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-white">{t('helpComprehensive.plugins.pluginDocumentation')}</h4> <h4 className="font-medium text-gray-900 dark:text-white">Automation Documentation</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">{t('helpComprehensive.plugins.pluginDocumentationDesc')}</p> <p className="text-sm text-gray-500 dark:text-gray-400">Complete guide to creating and configuring automations</p>
</div> </div>
<ChevronRight size={20} className="text-gray-400 ml-auto" /> <ChevronRight size={20} className="text-gray-400 ml-auto" />
</Link> </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.describe('Site Crawler - Tenant Dashboard', () => { test('should crawl tenant dashboard without cross-subdomain redirects', async ({ page, context }) => {
// test('should crawl tenant dashboard without errors', async ({ page, context }) => { const user = TEST_USERS.businessOwner;
// const user = TEST_USERS.businessOwner; const loggedIn = await loginAsUser(page, user);
// const loggedIn = await loginAsUser(page, user); expect(loggedIn).toBe(true);
// expect(loggedIn).toBe(true);
// const crawler = new SiteCrawler(page, context, {
// const crawler = new SiteCrawler(page, context, CRAWLER_OPTIONS); ...CRAWLER_OPTIONS,
// const report = await crawler.crawl('http://demo.lvh.me:5173'); detectCrossSubdomainRedirects: true, // Detect redirects to public site
// });
// console.log(formatReport(report)); const report = await crawler.crawl('http://demo.lvh.me:5173');
//
// expect(report.totalPages).toBeGreaterThan(0); console.log(formatReport(report));
//
// const criticalErrors = report.results.flatMap(r => expect(report.totalPages).toBeGreaterThan(0);
// r.errors.filter(e => {
// if (e.message.includes('React DevTools')) return false; // Filter critical errors (excluding minor dev warnings)
// if (e.message.includes('favicon.ico')) return false; const criticalErrors = report.results.flatMap(r =>
// if (e.message.includes('hot-update')) return false; r.errors.filter(e => {
// if (e.message.includes('WebSocket')) return false; if (e.message.includes('React DevTools')) return false;
// return true; 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); })
// }); );
// });
// 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.describe('Site Crawler - Customer Portal', () => {
// test('should crawl customer booking portal without errors', async ({ page, context }) => { // 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 { export interface CrawlError {
url: string; url: string;
type: 'console' | 'network' | 'broken-link' | 'page-error'; type: 'console' | 'network' | 'broken-link' | 'page-error' | 'redirect';
message: string; message: string;
details?: string; details?: string;
timestamp: Date; timestamp: Date;
@@ -33,6 +33,7 @@ export interface CrawlReport {
networkErrors: number; networkErrors: number;
brokenLinks: number; brokenLinks: number;
pageErrors: number; pageErrors: number;
redirects: number;
}; };
} }
@@ -45,6 +46,8 @@ export interface CrawlerOptions {
screenshotOnError?: boolean; screenshotOnError?: boolean;
screenshotDir?: string; screenshotDir?: string;
verbose?: boolean; verbose?: boolean;
/** Detect when page redirects to a different subdomain (e.g., tenant to public) */
detectCrossSubdomainRedirects?: boolean;
} }
const DEFAULT_OPTIONS: CrawlerOptions = { const DEFAULT_OPTIONS: CrawlerOptions = {
@@ -63,6 +66,7 @@ const DEFAULT_OPTIONS: CrawlerOptions = {
screenshotOnError: false, screenshotOnError: false,
screenshotDir: 'test-results/crawler-screenshots', screenshotDir: 'test-results/crawler-screenshots',
verbose: false, verbose: false,
detectCrossSubdomainRedirects: false,
}; };
export class SiteCrawler { 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 { private shouldCrawl(url: string): boolean {
// Skip if already visited // Skip if already visited
if (this.visited.has(url)) { 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 // Wait a bit for React to render and any async operations
await this.page.waitForTimeout(500); await this.page.waitForTimeout(500);
@@ -378,6 +417,7 @@ export class SiteCrawler {
networkErrors: 0, networkErrors: 0,
brokenLinks: 0, brokenLinks: 0,
pageErrors: 0, pageErrors: 0,
redirects: 0,
}; };
for (const result of this.results) { for (const result of this.results) {
@@ -395,6 +435,9 @@ export class SiteCrawler {
case 'page-error': case 'page-error':
summary.pageErrors++; summary.pageErrors++;
break; break;
case 'redirect':
summary.redirects++;
break;
} }
} }
} }
@@ -427,7 +470,8 @@ export function formatReport(report: CrawlReport): string {
output += ` Console errors: ${report.summary.consoleErrors}\n`; output += ` Console errors: ${report.summary.consoleErrors}\n`;
output += ` Network errors: ${report.summary.networkErrors}\n`; output += ` Network errors: ${report.summary.networkErrors}\n`;
output += ` Broken links: ${report.summary.brokenLinks}\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 // List pages with errors
const pagesWithErrors = report.results.filter(r => r.errors.length > 0); 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) { for (const error of result.errors) {
const icon = error.type === 'console' ? '⚠️' : const icon = error.type === 'console' ? '⚠️' :
error.type === 'network' ? '🌐' : error.type === 'network' ? '🌐' :
error.type === 'broken-link' ? '🔴' : '💥'; error.type === 'broken-link' ? '🔴' :
error.type === 'redirect' ? '↪️' : '💥';
output += ` ${icon} [${error.type.toUpperCase()}] ${error.message}\n`; output += ` ${icon} [${error.type.toUpperCase()}] ${error.message}\n`;
if (error.details) { if (error.details) {
output += ` Details: ${error.details.substring(0, 200)}\n`; output += ` Details: ${error.details.substring(0, 200)}\n`;