157 lines
5.5 KiB
TypeScript
157 lines
5.5 KiB
TypeScript
import React from 'react';
|
|
import { NavLink, useLocation } from 'react-router-dom';
|
|
import {
|
|
LayoutDashboard,
|
|
Calendar,
|
|
Mail,
|
|
Archive,
|
|
BarChart3,
|
|
Settings,
|
|
X,
|
|
Palette,
|
|
FileText,
|
|
HardDrive
|
|
} from 'lucide-react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { settingsService } from '../../services/settings.service';
|
|
import { VersionInfo } from './VersionInfo';
|
|
|
|
interface AdminSidebarProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface NavItem {
|
|
nameKey: string;
|
|
href: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
}
|
|
|
|
const navigation: NavItem[] = [
|
|
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
|
|
{ nameKey: 'navigation.events', href: '/admin/events', icon: Calendar },
|
|
{ nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive },
|
|
{ nameKey: 'admin.analytics', href: '/admin/analytics', icon: BarChart3 },
|
|
{ nameKey: 'navigation.emailSettings', href: '/admin/email', icon: Mail },
|
|
{ nameKey: 'navigation.branding', href: '/admin/branding', icon: Palette },
|
|
{ nameKey: 'navigation.settings', href: '/admin/settings', icon: Settings },
|
|
{ nameKey: 'navigation.backup', href: '/admin/backup', icon: HardDrive },
|
|
{ nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText },
|
|
];
|
|
|
|
export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) => {
|
|
const location = useLocation();
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<div
|
|
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-neutral-200 transform transition-transform duration-200 ease-in-out lg:relative lg:translate-x-0 lg:h-screen ${
|
|
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
}`}
|
|
>
|
|
<div className="flex flex-col h-screen lg:h-full">
|
|
{/* Brand */}
|
|
<div className="flex items-center justify-between h-16 px-6 border-b border-neutral-200 flex-shrink-0">
|
|
<div className="flex items-center">
|
|
<span className="text-xl font-bold text-neutral-900">{t('admin.title')}</span>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="lg:hidden text-neutral-400 hover:text-neutral-600"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 px-4 py-4 space-y-1 overflow-y-auto min-h-0">
|
|
{navigation.map((item) => {
|
|
const isActive = location.pathname === item.href ||
|
|
(item.href !== '/admin/dashboard' && location.pathname.startsWith(item.href));
|
|
|
|
return (
|
|
<NavLink
|
|
key={item.nameKey}
|
|
to={item.href}
|
|
onClick={() => onClose()}
|
|
className={`flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
|
isActive
|
|
? 'bg-primary-50 text-primary-700'
|
|
: 'text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900'
|
|
}`}
|
|
>
|
|
<item.icon className={`w-5 h-5 mr-3 ${
|
|
isActive ? 'text-primary-600' : 'text-neutral-400'
|
|
}`} />
|
|
{t(item.nameKey)}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Bottom section - sticky to bottom */}
|
|
<div className="flex-shrink-0">
|
|
{/* Version Info */}
|
|
<VersionInfo />
|
|
|
|
{/* Storage Info */}
|
|
<StorageInfo />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const StorageInfo: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const { data: storageInfo } = useQuery({
|
|
queryKey: ['storage-info'],
|
|
queryFn: () => settingsService.getStorageInfo(),
|
|
refetchInterval: 60000 // Refresh every minute
|
|
});
|
|
|
|
if (!storageInfo) {
|
|
return (
|
|
<div className="p-4 border-t border-neutral-200">
|
|
<div className="bg-neutral-100 rounded-lg p-3">
|
|
<div className="h-12 animate-pulse bg-neutral-200 rounded" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const limitInUse = storageInfo.storage_soft_limit || storageInfo.storage_limit || 1;
|
|
const usagePercent = limitInUse
|
|
? Math.round((storageInfo.total_used / limitInUse) * 100)
|
|
: 0;
|
|
const isOverSoftLimit = limitInUse && storageInfo.total_used >= limitInUse;
|
|
const progressBarClass = isOverSoftLimit ? 'bg-red-600' : 'bg-primary-600';
|
|
const containerClass = isOverSoftLimit
|
|
? 'bg-red-50 border border-red-200'
|
|
: 'bg-neutral-100';
|
|
const softLimitDisplay = settingsService.formatBytes(limitInUse);
|
|
|
|
return (
|
|
<div className="p-4 border-t border-neutral-200">
|
|
<div className={`${containerClass} rounded-lg p-3 transition-colors duration-300`}>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-neutral-700">{t('admin.storageUsed')}</span>
|
|
<span className="font-medium text-neutral-900">
|
|
{settingsService.formatBytes(storageInfo.total_used)}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 w-full bg-neutral-200 rounded-full h-2">
|
|
<div
|
|
className={`${progressBarClass} h-2 rounded-full transition-all duration-300`}
|
|
style={{ width: `${Math.min(usagePercent, 100)}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-neutral-600 mt-1">
|
|
{t('admin.storagePercent', { percent: usagePercent, limit: softLimitDisplay })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|