Initial commit: SmoothSchedule multi-tenant scheduling platform
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>
This commit is contained in:
174
frontend/src/components/Sidebar.tsx
Normal file
174
frontend/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user