Restructure navigation: move setup items to Settings with accordion menu

Move rarely-used setup items from main sidebar to Settings to keep
daily-use features prominent:

- Services → Settings > Business section
- Locations → Settings > Business section
- Site Builder → Settings > Branding section

Settings sidebar changes:
- Convert static sections to accordion (one open at a time)
- Auto-expand section based on current URL
- Preserve all permission checks for moved items

Add redirects from old URLs to new locations for backwards compatibility.

🤖 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-23 22:18:02 -05:00
parent 3eb1c303e5
commit e4668f81c5
5 changed files with 341 additions and 207 deletions

View File

@@ -879,15 +879,10 @@ const AppContent: React.FC = () => {
)
}
/>
{/* Redirect old services path to new settings location */}
<Route
path="/dashboard/services"
element={
canAccess('can_access_services') ? (
<Services />
) : (
<Navigate to="/dashboard" />
)
}
element={<Navigate to="/dashboard/settings/services" replace />}
/>
<Route
path="/dashboard/resources"
@@ -919,15 +914,10 @@ const AppContent: React.FC = () => {
)
}
/>
{/* Redirect old locations path to new settings location */}
<Route
path="/dashboard/locations"
element={
canAccess('can_access_locations') ? (
<Locations />
) : (
<Navigate to="/dashboard" />
)
}
element={<Navigate to="/dashboard/settings/locations" replace />}
/>
<Route
path="/dashboard/my-availability"
@@ -975,15 +965,10 @@ const AppContent: React.FC = () => {
)
}
/>
{/* Redirect old site-editor path to new settings location */}
<Route
path="/dashboard/site-editor"
element={
canAccess('can_access_site_editor') ? (
<PageEditor />
) : (
<Navigate to="/dashboard" />
)
}
element={<Navigate to="/dashboard/settings/site-builder" replace />}
/>
<Route
path="/dashboard/email-template-editor/:emailType"
@@ -1025,6 +1010,10 @@ const AppContent: React.FC = () => {
<Route path="sms-calling" element={<CommunicationSettings />} />
<Route path="billing" element={<BillingSettings />} />
<Route path="quota" element={<QuotaSettings />} />
{/* Moved from main sidebar */}
<Route path="services" element={<Services />} />
<Route path="locations" element={<Locations />} />
<Route path="site-builder" element={<PageEditor />} />
</Route>
) : (
<Route path="/dashboard/settings/*" element={<Navigate to="/dashboard" />} />

View File

@@ -10,15 +10,12 @@ import {
MessageSquare,
LogOut,
ClipboardList,
Briefcase,
Ticket,
HelpCircle,
Clock,
Plug,
FileSignature,
CalendarOff,
LayoutTemplate,
MapPin,
Image,
} from 'lucide-react';
import { Business, User } from '../types';
@@ -159,25 +156,14 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
{/* Manage Section - Show if user has any manage-related permission */}
{(canViewManagementPages ||
hasPermission('can_access_site_builder') ||
hasPermission('can_access_gallery') ||
hasPermission('can_access_customers') ||
hasPermission('can_access_services') ||
hasPermission('can_access_resources') ||
hasPermission('can_access_staff') ||
hasPermission('can_access_contracts') ||
hasPermission('can_access_time_blocks') ||
hasPermission('can_access_locations')
hasPermission('can_access_time_blocks')
) && (
<SidebarSection title={t('nav.sections.manage', 'Manage')} isCollapsed={isCollapsed}>
{hasPermission('can_access_site_builder') && (
<SidebarItem
to="/dashboard/site-editor"
icon={LayoutTemplate}
label={t('nav.siteBuilder', 'Site Builder')}
isCollapsed={isCollapsed}
/>
)}
{hasPermission('can_access_gallery') && (
<SidebarItem
to="/dashboard/gallery"
@@ -194,14 +180,6 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
isCollapsed={isCollapsed}
/>
)}
{hasPermission('can_access_services') && (
<SidebarItem
to="/dashboard/services"
icon={Briefcase}
label={t('nav.services', 'Services')}
isCollapsed={isCollapsed}
/>
)}
{hasPermission('can_access_resources') && (
<SidebarItem
to="/dashboard/resources"
@@ -235,15 +213,6 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
isCollapsed={isCollapsed}
/>
)}
{hasPermission('can_access_locations') && (
<SidebarItem
to="/dashboard/locations"
icon={MapPin}
label={t('nav.locations', 'Locations')}
isCollapsed={isCollapsed}
locked={!canUse('multi_location')}
/>
)}
</SidebarSection>
)}

View File

@@ -256,6 +256,55 @@ export const SettingsSidebarSection: React.FC<SettingsSidebarSectionProps> = ({
);
};
interface SettingsAccordionSectionProps {
title: string;
sectionKey: string;
isOpen: boolean;
onToggle: (sectionKey: string) => void;
children: React.ReactNode;
hasVisibleItems?: boolean;
}
/**
* Collapsible accordion section for settings sidebar
* Only one section can be open at a time (controlled by parent)
*/
export const SettingsAccordionSection: React.FC<SettingsAccordionSectionProps> = ({
title,
sectionKey,
isOpen,
onToggle,
children,
hasVisibleItems = true,
}) => {
// Don't render if no visible items
if (!hasVisibleItems) return null;
return (
<div className="space-y-0.5">
<button
onClick={() => onToggle(sectionKey)}
className="flex items-center justify-between w-full px-4 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<span>{title}</span>
<ChevronDown
size={14}
className={`transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${
isOpen ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'
}`}
>
<div className="space-y-0.5 pt-1">
{children}
</div>
</div>
</div>
);
};
interface SettingsSidebarItemProps {
to: string;
icon: LucideIcon;

View File

@@ -2042,6 +2042,14 @@
"title": "Business Hours",
"description": "Operating hours"
},
"services": {
"title": "Services",
"description": "Manage your services"
},
"locations": {
"title": "Locations",
"description": "Business locations"
},
"appearance": {
"title": "Appearance",
"description": "Logo, colors, theme"
@@ -2082,6 +2090,10 @@
"step2": "Copy the embed code and paste it into your website's HTML where you want the booking widget to appear.",
"step3": "For platforms like WordPress, Squarespace, or Wix, look for an \"HTML\" or \"Embed\" block and paste the code there."
},
"siteBuilder": {
"title": "Site Builder",
"description": "Build your booking site"
},
"api": {
"title": "API & Webhooks",
"description": "API tokens, webhooks"

View File

@@ -5,7 +5,7 @@
* Used as a wrapper for all /settings/* routes.
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Outlet, Link, useLocation, useNavigate, useOutletContext } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
@@ -24,9 +24,12 @@ import {
Clock,
Users,
Code2,
Briefcase,
MapPin,
LayoutTemplate,
} from 'lucide-react';
import {
SettingsSidebarSection,
SettingsAccordionSection,
SettingsSidebarItem,
} from '../components/navigation/SidebarComponents';
import UnfinishedBadge from '../components/ui/UnfinishedBadge';
@@ -46,6 +49,44 @@ const SETTINGS_PAGE_FEATURES: Record<string, FeatureKey> = {
'/dashboard/settings/api': 'api_access',
'/dashboard/settings/authentication': 'custom_oauth',
'/dashboard/settings/sms-calling': 'sms_reminders',
'/dashboard/settings/locations': 'multi_location',
};
// Map URL paths to section keys for auto-expand
const URL_TO_SECTION: Record<string, string> = {
'/dashboard/settings/general': 'business',
'/dashboard/settings/resource-types': 'business',
'/dashboard/settings/booking': 'business',
'/dashboard/settings/business-hours': 'business',
'/dashboard/settings/services': 'business',
'/dashboard/settings/locations': 'business',
'/dashboard/settings/branding': 'branding',
'/dashboard/settings/email-templates': 'branding',
'/dashboard/settings/custom-domains': 'branding',
'/dashboard/settings/embed-widget': 'branding',
'/dashboard/settings/site-builder': 'branding',
'/dashboard/settings/api': 'integrations',
'/dashboard/settings/staff-roles': 'access',
'/dashboard/settings/authentication': 'access',
'/dashboard/settings/email': 'communication',
'/dashboard/settings/sms-calling': 'communication',
'/dashboard/settings/billing': 'billing',
'/dashboard/settings/quota': 'billing',
};
// Helper to get section from URL
const getSectionFromUrl = (pathname: string): string => {
// Check for exact match first
if (URL_TO_SECTION[pathname]) {
return URL_TO_SECTION[pathname];
}
// Check for prefix match (for nested routes)
for (const [path, section] of Object.entries(URL_TO_SECTION)) {
if (pathname.startsWith(path)) {
return section;
}
}
return 'business'; // Default to business section
};
const SettingsLayout: React.FC = () => {
@@ -59,6 +100,20 @@ const SettingsLayout: React.FC = () => {
const { user } = parentContext || {};
const isOwner = user?.role === 'owner';
// Accordion state - track which section is open
const [openSection, setOpenSection] = useState<string>(() => getSectionFromUrl(location.pathname));
// Update open section when URL changes
useEffect(() => {
const section = getSectionFromUrl(location.pathname);
setOpenSection(section);
}, [location.pathname]);
// Handle section toggle (only one open at a time)
const handleSectionToggle = (sectionKey: string) => {
setOpenSection(prev => prev === sectionKey ? '' : sectionKey);
};
// Check if staff has access to a specific settings page
const hasSettingsPermission = (permissionKey: string): boolean => {
// Owners always have all permissions
@@ -100,13 +155,22 @@ const SettingsLayout: React.FC = () => {
</div>
{/* Navigation */}
<nav className="flex-1 px-2 pb-4 space-y-3 overflow-y-auto">
<nav className="flex-1 px-2 pb-4 space-y-1 overflow-y-auto">
{/* Business Section */}
{(hasSettingsPermission('can_access_settings_general') ||
<SettingsAccordionSection
title={t('settings.sections.business', 'Business')}
sectionKey="business"
isOpen={openSection === 'business'}
onToggle={handleSectionToggle}
hasVisibleItems={
hasSettingsPermission('can_access_settings_general') ||
hasSettingsPermission('can_access_settings_resource_types') ||
hasSettingsPermission('can_access_settings_booking') ||
hasSettingsPermission('can_access_settings_business_hours')) && (
<SettingsSidebarSection title={t('settings.sections.business', 'Business')}>
hasSettingsPermission('can_access_settings_business_hours') ||
hasSettingsPermission('can_access_services') ||
hasSettingsPermission('can_access_locations')
}
>
{hasSettingsPermission('can_access_settings_general') && (
<SettingsSidebarItem
to="/dashboard/settings/general"
@@ -139,15 +203,39 @@ const SettingsLayout: React.FC = () => {
description={t('settings.businessHours.description', 'Operating hours')}
/>
)}
</SettingsSidebarSection>
{hasSettingsPermission('can_access_services') && (
<SettingsSidebarItem
to="/dashboard/settings/services"
icon={Briefcase}
label={t('settings.services.title', 'Services')}
description={t('settings.services.description', 'Manage your services')}
/>
)}
{hasSettingsPermission('can_access_locations') && (
<SettingsSidebarItem
to="/dashboard/settings/locations"
icon={MapPin}
label={t('settings.locations.title', 'Locations')}
description={t('settings.locations.description', 'Business locations')}
locked={isLocked('multi_location')}
/>
)}
</SettingsAccordionSection>
{/* Branding Section */}
{(hasSettingsPermission('can_access_settings_branding') ||
<SettingsAccordionSection
title={t('settings.sections.branding', 'Branding')}
sectionKey="branding"
isOpen={openSection === 'branding'}
onToggle={handleSectionToggle}
hasVisibleItems={
hasSettingsPermission('can_access_settings_branding') ||
hasSettingsPermission('can_access_settings_email_templates') ||
hasSettingsPermission('can_access_settings_custom_domains') ||
hasSettingsPermission('can_access_settings_embed_widget')) && (
<SettingsSidebarSection title={t('settings.sections.branding', 'Branding')}>
hasSettingsPermission('can_access_settings_embed_widget') ||
hasSettingsPermission('can_access_site_builder')
}
>
{hasSettingsPermission('can_access_settings_branding') && (
<SettingsSidebarItem
to="/dashboard/settings/branding"
@@ -182,12 +270,24 @@ const SettingsLayout: React.FC = () => {
description={t('settings.embedWidget.sidebarDescription', 'Add booking to your site')}
/>
)}
</SettingsSidebarSection>
{hasSettingsPermission('can_access_site_builder') && (
<SettingsSidebarItem
to="/dashboard/settings/site-builder"
icon={LayoutTemplate}
label={t('settings.siteBuilder.title', 'Site Builder')}
description={t('settings.siteBuilder.description', 'Build your booking site')}
/>
)}
</SettingsAccordionSection>
{/* Integrations Section */}
{hasSettingsPermission('can_access_settings_api') && (
<SettingsSidebarSection title={t('settings.sections.integrations', 'Integrations')}>
<SettingsAccordionSection
title={t('settings.sections.integrations', 'Integrations')}
sectionKey="integrations"
isOpen={openSection === 'integrations'}
onToggle={handleSectionToggle}
hasVisibleItems={hasSettingsPermission('can_access_settings_api')}
>
<SettingsSidebarItem
to="/dashboard/settings/api"
icon={Key}
@@ -195,13 +295,19 @@ const SettingsLayout: React.FC = () => {
description={t('settings.api.description', 'API tokens, webhooks')}
locked={isLocked('api_access')}
/>
</SettingsSidebarSection>
)}
</SettingsAccordionSection>
{/* Access Section */}
{(hasSettingsPermission('can_access_settings_staff_roles') ||
hasSettingsPermission('can_access_settings_authentication')) && (
<SettingsSidebarSection title={t('settings.sections.access', 'Access')}>
<SettingsAccordionSection
title={t('settings.sections.access', 'Access')}
sectionKey="access"
isOpen={openSection === 'access'}
onToggle={handleSectionToggle}
hasVisibleItems={
hasSettingsPermission('can_access_settings_staff_roles') ||
hasSettingsPermission('can_access_settings_authentication')
}
>
{hasSettingsPermission('can_access_settings_staff_roles') && (
<SettingsSidebarItem
to="/dashboard/settings/staff-roles"
@@ -219,13 +325,19 @@ const SettingsLayout: React.FC = () => {
locked={isLocked('custom_oauth')}
/>
)}
</SettingsSidebarSection>
)}
</SettingsAccordionSection>
{/* Communication Section */}
{(hasSettingsPermission('can_access_settings_email') ||
hasSettingsPermission('can_access_settings_sms_calling')) && (
<SettingsSidebarSection title={t('settings.sections.communication', 'Communication')}>
<SettingsAccordionSection
title={t('settings.sections.communication', 'Communication')}
sectionKey="communication"
isOpen={openSection === 'communication'}
onToggle={handleSectionToggle}
hasVisibleItems={
hasSettingsPermission('can_access_settings_email') ||
hasSettingsPermission('can_access_settings_sms_calling')
}
>
{hasSettingsPermission('can_access_settings_email') && (
<SettingsSidebarItem
to="/dashboard/settings/email"
@@ -243,12 +355,16 @@ const SettingsLayout: React.FC = () => {
locked={isLocked('sms_reminders')}
/>
)}
</SettingsSidebarSection>
)}
</SettingsAccordionSection>
{/* Billing Section - Owner only */}
{isOwner && (
<SettingsSidebarSection title={t('settings.sections.billing', 'Billing')}>
<SettingsAccordionSection
title={t('settings.sections.billing', 'Billing')}
sectionKey="billing"
isOpen={openSection === 'billing'}
onToggle={handleSectionToggle}
hasVisibleItems={isOwner}
>
<SettingsSidebarItem
to="/dashboard/settings/billing"
icon={CreditCard}
@@ -261,8 +377,7 @@ const SettingsLayout: React.FC = () => {
label={t('settings.quota.title', 'Quota Management')}
description={t('settings.quota.description', 'Usage limits, archiving')}
/>
</SettingsSidebarSection>
)}
</SettingsAccordionSection>
</nav>
</aside>