feat: Email templates, bulk delete, communication credits, plan features
- Add email template presets for Browse Templates tab (12 templates) - Add bulk selection and deletion for My Templates tab - Add communication credits system with Twilio integration - Add payment views for credit purchases and auto-reload - Add SMS reminder and masked calling plan permissions - Fix appointment status mapping (frontend/backend mismatch) - Clear masquerade stack on login/logout for session hygiene - Update platform settings with credit configuration - Add new migrations for Twilio and Stripe payment fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
281
frontend/src/components/navigation/SidebarComponents.tsx
Normal file
281
frontend/src/components/navigation/SidebarComponents.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 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, 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation item with icon
|
||||
*/
|
||||
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||
to,
|
||||
icon: Icon,
|
||||
label,
|
||||
isCollapsed = false,
|
||||
exact = false,
|
||||
disabled = false,
|
||||
badge,
|
||||
variant = 'default',
|
||||
}) => {
|
||||
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'
|
||||
: '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'
|
||||
: '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">{label}</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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings navigation item with optional description
|
||||
*/
|
||||
export const SettingsSidebarItem: React.FC<SettingsSidebarItemProps> = ({
|
||||
to,
|
||||
icon: Icon,
|
||||
label,
|
||||
description,
|
||||
}) => {
|
||||
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'
|
||||
: '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">
|
||||
<span className="font-medium">{label}</span>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500 truncate">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user