- Add locked state to Plugins sidebar item with plan feature check - Create Branding section in settings with Appearance, Email Templates, Custom Domains - Split Domains page into Booking (URLs, redirects) and Custom Domains (BYOD, purchase) - Add booking_return_url field to Tenant model for customer redirects - Update SidebarItem component to support locked prop with lock icon - Move Email Templates from main sidebar to Settings > Branding - Add communication credits hooks and payment form updates - Add timezone fields migration and various UI improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
302 lines
7.9 KiB
TypeScript
302 lines
7.9 KiB
TypeScript
/**
|
|
* Shared Sidebar Navigation Components
|
|
*
|
|
* Reusable building blocks for main sidebar and settings sidebar navigation.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
import { ChevronDown, Lock, LucideIcon } from 'lucide-react';
|
|
|
|
interface SidebarSectionProps {
|
|
title?: string;
|
|
children: React.ReactNode;
|
|
isCollapsed?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Section wrapper with optional header
|
|
*/
|
|
export const SidebarSection: React.FC<SidebarSectionProps> = ({
|
|
title,
|
|
children,
|
|
isCollapsed = false,
|
|
className = '',
|
|
}) => {
|
|
return (
|
|
<div className={`space-y-1 ${className}`}>
|
|
{title && !isCollapsed && (
|
|
<h3 className="px-4 pt-1 pb-1.5 text-xs font-semibold uppercase tracking-wider text-white/40">
|
|
{title}
|
|
</h3>
|
|
)}
|
|
{title && isCollapsed && (
|
|
<div className="mx-auto w-8 border-t border-white/20 my-2" />
|
|
)}
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface SidebarItemProps {
|
|
to: string;
|
|
icon: LucideIcon;
|
|
label: string;
|
|
isCollapsed?: boolean;
|
|
exact?: boolean;
|
|
disabled?: boolean;
|
|
badge?: string | number;
|
|
variant?: 'default' | 'settings';
|
|
locked?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Navigation item with icon
|
|
*/
|
|
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
|
to,
|
|
icon: Icon,
|
|
label,
|
|
isCollapsed = false,
|
|
exact = false,
|
|
disabled = false,
|
|
badge,
|
|
variant = 'default',
|
|
locked = false,
|
|
}) => {
|
|
const location = useLocation();
|
|
const isActive = exact
|
|
? location.pathname === to
|
|
: location.pathname.startsWith(to);
|
|
|
|
const baseClasses = 'flex items-center gap-3 py-2.5 text-sm font-medium rounded-lg transition-colors';
|
|
const collapsedClasses = isCollapsed ? 'px-3 justify-center' : 'px-4';
|
|
|
|
// Different color schemes for main nav vs settings nav
|
|
const colorClasses = variant === 'settings'
|
|
? isActive
|
|
? 'bg-brand-50 text-brand-700 dark:bg-brand-900/30 dark:text-brand-400'
|
|
: locked
|
|
? 'text-gray-400 hover:text-gray-500 hover:bg-gray-50 dark:text-gray-500 dark:hover:text-gray-400 dark:hover:bg-gray-800'
|
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-800'
|
|
: isActive
|
|
? 'bg-white/10 text-white'
|
|
: locked
|
|
? 'text-white/40 hover:text-white/60 hover:bg-white/5'
|
|
: 'text-white/70 hover:text-white hover:bg-white/5';
|
|
|
|
const disabledClasses = variant === 'settings'
|
|
? 'text-gray-300 dark:text-gray-600 cursor-not-allowed'
|
|
: 'text-white/30 cursor-not-allowed';
|
|
|
|
const className = `${baseClasses} ${collapsedClasses} ${disabled ? disabledClasses : colorClasses}`;
|
|
|
|
if (disabled) {
|
|
return (
|
|
<div className={className} title={label}>
|
|
<Icon size={20} className="shrink-0" />
|
|
{!isCollapsed && <span className="flex-1">{label}</span>}
|
|
{badge && !isCollapsed && (
|
|
<span className="px-2 py-0.5 text-xs rounded-full bg-white/10">{badge}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link to={to} className={className} title={label}>
|
|
<Icon size={20} className="shrink-0" />
|
|
{!isCollapsed && (
|
|
<span className="flex-1 flex items-center gap-1.5">
|
|
{label}
|
|
{locked && <Lock size={12} className="opacity-60" />}
|
|
</span>
|
|
)}
|
|
{badge && !isCollapsed && (
|
|
<span className="px-2 py-0.5 text-xs rounded-full bg-brand-100 text-brand-700 dark:bg-brand-900/50 dark:text-brand-400">
|
|
{badge}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
interface SidebarDropdownProps {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
children: React.ReactNode;
|
|
isCollapsed?: boolean;
|
|
defaultOpen?: boolean;
|
|
isActiveWhen?: string[];
|
|
}
|
|
|
|
/**
|
|
* Collapsible dropdown section
|
|
*/
|
|
export const SidebarDropdown: React.FC<SidebarDropdownProps> = ({
|
|
icon: Icon,
|
|
label,
|
|
children,
|
|
isCollapsed = false,
|
|
defaultOpen = false,
|
|
isActiveWhen = [],
|
|
}) => {
|
|
const location = useLocation();
|
|
const [isOpen, setIsOpen] = React.useState(
|
|
defaultOpen || isActiveWhen.some(path => location.pathname.startsWith(path))
|
|
);
|
|
|
|
const isActive = isActiveWhen.some(path => location.pathname.startsWith(path));
|
|
|
|
return (
|
|
<div>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className={`flex items-center gap-3 py-2.5 text-sm font-medium rounded-lg transition-colors w-full ${
|
|
isCollapsed ? 'px-3 justify-center' : 'px-4'
|
|
} ${
|
|
isActive
|
|
? 'bg-white/10 text-white'
|
|
: 'text-white/70 hover:text-white hover:bg-white/5'
|
|
}`}
|
|
title={label}
|
|
>
|
|
<Icon size={20} className="shrink-0" />
|
|
{!isCollapsed && (
|
|
<>
|
|
<span className="flex-1 text-left">{label}</span>
|
|
<ChevronDown
|
|
size={16}
|
|
className={`shrink-0 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
/>
|
|
</>
|
|
)}
|
|
</button>
|
|
{isOpen && !isCollapsed && (
|
|
<div className="ml-4 mt-1 space-y-0.5 border-l border-white/20 pl-4">
|
|
{children}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface SidebarSubItemProps {
|
|
to: string;
|
|
icon: LucideIcon;
|
|
label: string;
|
|
}
|
|
|
|
/**
|
|
* Sub-item for dropdown menus
|
|
*/
|
|
export const SidebarSubItem: React.FC<SidebarSubItemProps> = ({
|
|
to,
|
|
icon: Icon,
|
|
label,
|
|
}) => {
|
|
const location = useLocation();
|
|
const isActive = location.pathname === to;
|
|
|
|
return (
|
|
<Link
|
|
to={to}
|
|
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${
|
|
isActive
|
|
? 'bg-white/10 text-white'
|
|
: 'text-white/60 hover:text-white hover:bg-white/5'
|
|
}`}
|
|
title={label}
|
|
>
|
|
<Icon size={16} className="shrink-0" />
|
|
<span>{label}</span>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
interface SidebarDividerProps {
|
|
isCollapsed?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Visual divider between sections
|
|
*/
|
|
export const SidebarDivider: React.FC<SidebarDividerProps> = ({ isCollapsed }) => {
|
|
return (
|
|
<div className={`my-4 ${isCollapsed ? 'mx-3' : 'mx-4'} border-t border-white/10`} />
|
|
);
|
|
};
|
|
|
|
interface SettingsSidebarSectionProps {
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Section for settings sidebar (different styling)
|
|
*/
|
|
export const SettingsSidebarSection: React.FC<SettingsSidebarSectionProps> = ({
|
|
title,
|
|
children,
|
|
}) => {
|
|
return (
|
|
<div className="space-y-0.5">
|
|
<h3 className="px-4 pt-0.5 pb-1 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
|
{title}
|
|
</h3>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface SettingsSidebarItemProps {
|
|
to: string;
|
|
icon: LucideIcon;
|
|
label: string;
|
|
description?: string;
|
|
locked?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Settings navigation item with optional description and lock indicator
|
|
*/
|
|
export const SettingsSidebarItem: React.FC<SettingsSidebarItemProps> = ({
|
|
to,
|
|
icon: Icon,
|
|
label,
|
|
description,
|
|
locked = false,
|
|
}) => {
|
|
const location = useLocation();
|
|
const isActive = location.pathname === to || location.pathname.startsWith(to + '/');
|
|
|
|
return (
|
|
<Link
|
|
to={to}
|
|
className={`flex items-start gap-2.5 px-4 py-1.5 text-sm rounded-lg transition-colors ${
|
|
isActive
|
|
? 'bg-brand-50 text-brand-700 dark:bg-brand-900/30 dark:text-brand-400'
|
|
: locked
|
|
? 'text-gray-400 hover:text-gray-500 hover:bg-gray-50 dark:text-gray-500 dark:hover:text-gray-400 dark:hover:bg-gray-800'
|
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-800'
|
|
}`}
|
|
>
|
|
<Icon size={16} className="shrink-0 mt-0.5" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="font-medium">{label}</span>
|
|
{locked && (
|
|
<Lock size={12} className="text-gray-400 dark:text-gray-500" />
|
|
)}
|
|
</div>
|
|
{description && (
|
|
<p className="text-xs text-gray-500 dark:text-gray-500 truncate">
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
);
|
|
};
|