This commit includes: - Django backend with multi-tenancy (django-tenants) - React + TypeScript frontend with Vite - Platform administration API with role-based access control - Authentication system with token-based auth - Quick login dev tools for testing different user roles - CORS and CSRF configuration for local development - Docker development environment setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
174 lines
6.8 KiB
TypeScript
174 lines
6.8 KiB
TypeScript
import React from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
import {
|
|
LayoutDashboard,
|
|
CalendarDays,
|
|
Settings,
|
|
Users,
|
|
CreditCard,
|
|
MessageSquare,
|
|
LogOut,
|
|
ClipboardList,
|
|
Briefcase
|
|
} from 'lucide-react';
|
|
import { Business, User } from '../types';
|
|
import { useLogout } from '../hooks/useAuth';
|
|
import SmoothScheduleLogo from './SmoothScheduleLogo';
|
|
|
|
interface SidebarProps {
|
|
business: Business;
|
|
user: User;
|
|
isCollapsed: boolean;
|
|
toggleCollapse: () => void;
|
|
}
|
|
|
|
const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCollapse }) => {
|
|
const { t } = useTranslation();
|
|
const location = useLocation();
|
|
const { role } = user;
|
|
const logoutMutation = useLogout();
|
|
|
|
const getNavClass = (path: string, exact: boolean = false, disabled: boolean = false) => {
|
|
const isActive = exact
|
|
? location.pathname === path
|
|
: location.pathname.startsWith(path);
|
|
|
|
const baseClasses = `flex items-center gap-3 py-3 text-sm font-medium rounded-lg transition-colors`;
|
|
const collapsedClasses = isCollapsed ? 'px-3 justify-center' : 'px-4';
|
|
const activeClasses = 'bg-opacity-10 text-white bg-white';
|
|
const inactiveClasses = 'text-white/70 hover:text-white hover:bg-white/5';
|
|
const disabledClasses = 'text-white/30 cursor-not-allowed';
|
|
|
|
if (disabled) {
|
|
return `${baseClasses} ${collapsedClasses} ${disabledClasses}`;
|
|
}
|
|
|
|
return `${baseClasses} ${collapsedClasses} ${isActive ? activeClasses : inactiveClasses}`;
|
|
};
|
|
|
|
const canViewAdminPages = role === 'owner' || role === 'manager';
|
|
const canViewManagementPages = role === 'owner' || role === 'manager' || role === 'staff';
|
|
const canViewSettings = role === 'owner';
|
|
|
|
const getDashboardLink = () => {
|
|
if (role === 'resource') return '/';
|
|
return '/';
|
|
};
|
|
|
|
const handleSignOut = () => {
|
|
logoutMutation.mutate();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`flex flex-col h-full text-white shrink-0 transition-all duration-300 ${isCollapsed ? 'w-20' : 'w-64'}`}
|
|
style={{ backgroundColor: business.primaryColor }}
|
|
>
|
|
<button
|
|
onClick={toggleCollapse}
|
|
className={`flex items-center gap-3 w-full text-left px-6 py-8 ${isCollapsed ? 'justify-center' : ''} hover:bg-white/5 transition-colors focus:outline-none`}
|
|
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
>
|
|
<div className="flex items-center justify-center w-10 h-10 bg-white rounded-lg text-brand-600 font-bold text-xl shrink-0" style={{ color: business.primaryColor }}>
|
|
{business.name.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
{!isCollapsed && (
|
|
<div className="overflow-hidden">
|
|
<h1 className="font-bold leading-tight truncate">{business.name}</h1>
|
|
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
|
|
</div>
|
|
)}
|
|
</button>
|
|
|
|
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
|
<Link to={getDashboardLink()} className={getNavClass('/', true)} title={t('nav.dashboard')}>
|
|
<LayoutDashboard size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.dashboard')}</span>}
|
|
</Link>
|
|
|
|
<Link to="/scheduler" className={getNavClass('/scheduler')} title={t('nav.scheduler')}>
|
|
<CalendarDays size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.scheduler')}</span>}
|
|
</Link>
|
|
|
|
{canViewManagementPages && (
|
|
<>
|
|
<Link to="/customers" className={getNavClass('/customers')} title={t('nav.customers')}>
|
|
<Users size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.customers')}</span>}
|
|
</Link>
|
|
<Link to="/services" className={getNavClass('/services')} title={t('nav.services', 'Services')}>
|
|
<Briefcase size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.services', 'Services')}</span>}
|
|
</Link>
|
|
<Link to="/resources" className={getNavClass('/resources')} title={t('nav.resources')}>
|
|
<ClipboardList size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.resources')}</span>}
|
|
</Link>
|
|
</>
|
|
)}
|
|
|
|
{canViewAdminPages && (
|
|
<>
|
|
{business.paymentsEnabled ? (
|
|
<Link to="/payments" className={getNavClass('/payments')} title={t('nav.payments')}>
|
|
<CreditCard size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
|
</Link>
|
|
) : (
|
|
<div
|
|
className={getNavClass('/payments', false, true)}
|
|
title={t('nav.paymentsDisabledTooltip')}
|
|
>
|
|
<CreditCard size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.payments')}</span>}
|
|
</div>
|
|
)}
|
|
<Link to="/messages" className={getNavClass('/messages')} title={t('nav.messages')}>
|
|
<MessageSquare size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.messages')}</span>}
|
|
</Link>
|
|
<Link to="/staff" className={getNavClass('/staff')} title={t('nav.staff')}>
|
|
<Users size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.staff')}</span>}
|
|
</Link>
|
|
</>
|
|
)}
|
|
|
|
{canViewSettings && (
|
|
<div className="pt-8 mt-8 border-t border-white/10">
|
|
{canViewSettings && (
|
|
<Link to="/settings" className={getNavClass('/settings', true)} title={t('nav.businessSettings')}>
|
|
<Settings size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('nav.businessSettings')}</span>}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t border-white/10">
|
|
<div className={`flex items-center gap-2 text-xs text-white/60 mb-4 ${isCollapsed ? 'justify-center' : ''}`}>
|
|
<SmoothScheduleLogo className="w-6 h-6 text-white" />
|
|
{!isCollapsed && (
|
|
<div>
|
|
<span className="block">{t('common.poweredBy')}</span>
|
|
<span className="font-semibold text-white/80">Smooth Schedule</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={handleSignOut}
|
|
disabled={logoutMutation.isPending}
|
|
className={`flex items-center gap-3 px-4 py-2 text-sm font-medium text-white/70 hover:text-white w-full transition-colors rounded-lg ${isCollapsed ? 'justify-center' : ''} disabled:opacity-50`}
|
|
>
|
|
<LogOut size={20} className="shrink-0" />
|
|
{!isCollapsed && <span>{t('auth.signOut')}</span>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Sidebar; |