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:
@@ -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 />} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,68 +19,127 @@ describe('FloatingHelpButton', () => {
|
||||
);
|
||||
};
|
||||
|
||||
it('renders help link on dashboard', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toBeInTheDocument();
|
||||
describe('tenant dashboard routes (prefixed with /dashboard)', () => {
|
||||
it('renders help link on tenant dashboard', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/dashboard for /dashboard', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/dashboard');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/scheduler for /dashboard/scheduler', () => {
|
||||
renderWithRouter('/dashboard/scheduler');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/scheduler');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/services for /dashboard/services', () => {
|
||||
renderWithRouter('/dashboard/services');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/services');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/resources for /dashboard/resources', () => {
|
||||
renderWithRouter('/dashboard/resources');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/resources');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/settings/general for /dashboard/settings/general', () => {
|
||||
renderWithRouter('/dashboard/settings/general');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/settings/general');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/customers for /dashboard/customers/123', () => {
|
||||
renderWithRouter('/dashboard/customers/123');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/customers');
|
||||
});
|
||||
|
||||
it('returns null on /dashboard/help pages', () => {
|
||||
const { container } = renderWithRouter('/dashboard/help/dashboard');
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('links to /dashboard/help for unknown dashboard routes', () => {
|
||||
renderWithRouter('/dashboard/unknown-route');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/site-builder for /dashboard/site-editor', () => {
|
||||
renderWithRouter('/dashboard/site-editor');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/site-builder');
|
||||
});
|
||||
|
||||
it('links to /dashboard/help/site-builder for /dashboard/gallery', () => {
|
||||
renderWithRouter('/dashboard/gallery');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/dashboard/help/site-builder');
|
||||
});
|
||||
});
|
||||
|
||||
it('links to correct help page for dashboard', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/dashboard');
|
||||
describe('non-dashboard routes (public/platform)', () => {
|
||||
it('links to /help/scheduler for /scheduler', () => {
|
||||
renderWithRouter('/scheduler');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/scheduler');
|
||||
});
|
||||
|
||||
it('links to /help/services for /services', () => {
|
||||
renderWithRouter('/services');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/services');
|
||||
});
|
||||
|
||||
it('links to /help/resources for /resources', () => {
|
||||
renderWithRouter('/resources');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/resources');
|
||||
});
|
||||
|
||||
it('links to /help/settings/general for /settings/general', () => {
|
||||
renderWithRouter('/settings/general');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/settings/general');
|
||||
});
|
||||
|
||||
it('returns null on /help pages', () => {
|
||||
const { container } = renderWithRouter('/help/dashboard');
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('links to /help for unknown routes', () => {
|
||||
renderWithRouter('/unknown-route');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help');
|
||||
});
|
||||
|
||||
it('handles dynamic routes by matching prefix', () => {
|
||||
renderWithRouter('/customers/123');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/customers');
|
||||
});
|
||||
});
|
||||
|
||||
it('links to correct help page for scheduler', () => {
|
||||
renderWithRouter('/scheduler');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/scheduler');
|
||||
});
|
||||
describe('accessibility', () => {
|
||||
it('has aria-label', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('aria-label', 'Help');
|
||||
});
|
||||
|
||||
it('links to correct help page for services', () => {
|
||||
renderWithRouter('/services');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/services');
|
||||
});
|
||||
|
||||
it('links to correct help page for resources', () => {
|
||||
renderWithRouter('/resources');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/resources');
|
||||
});
|
||||
|
||||
it('links to correct help page for settings', () => {
|
||||
renderWithRouter('/settings/general');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/settings/general');
|
||||
});
|
||||
|
||||
it('returns null on help pages', () => {
|
||||
const { container } = renderWithRouter('/help/dashboard');
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('has aria-label', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('aria-label', 'Help');
|
||||
});
|
||||
|
||||
it('has title attribute', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('title', 'Help');
|
||||
});
|
||||
|
||||
it('links to default help for unknown routes', () => {
|
||||
renderWithRouter('/unknown-route');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help');
|
||||
});
|
||||
|
||||
it('handles dynamic routes by matching prefix', () => {
|
||||
renderWithRouter('/customers/123');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', '/help/customers');
|
||||
it('has title attribute', () => {
|
||||
renderWithRouter('/dashboard');
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('title', 'Help');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 → Marketplace to explore available plugins.</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Go to Automations → 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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
526
frontend/src/pages/help/HelpSiteBuilder.tsx
Normal file
526
frontend/src/pages/help/HelpSiteBuilder.tsx
Normal 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;
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user