[feature/frontend] admin panel (wip)
Some checks failed
Build Backend / Build Docker Image (push) Failing after 2m38s
Test Backend / test (push) Successful in 4m19s

This commit is contained in:
CDN 2025-02-21 07:55:26 +08:00
parent 34ebb05808
commit 1526c27b49
Signed by: CDN
GPG key ID: 0C656827F9F80080
18 changed files with 1130 additions and 22 deletions

View file

@ -0,0 +1,168 @@
import { FC } from 'react';
import { Link, Outlet, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
RiFileTextLine,
RiFolderLine,
RiUserLine,
RiTeamLine,
RiLogoutBoxRLine,
RiSunLine,
RiMoonLine,
RiComputerLine,
RiGlobalLine
} from 'react-icons/ri';
import { useTheme } from '../../../hooks/useTheme';
import type { Theme } from '../../../hooks/useTheme';
interface AdminLayoutProps {}
const menuItems = [
{ path: '/admin/posts', icon: RiFileTextLine, label: 'admin.nav.posts' },
{ path: '/admin/categories', icon: RiFolderLine, label: 'admin.nav.categories' },
{ path: '/admin/users', icon: RiUserLine, label: 'admin.nav.users' },
{ path: '/admin/contributors', icon: RiTeamLine, label: 'admin.nav.contributors' },
];
const themeOptions = [
{ value: 'light' as const, icon: RiSunLine, label: 'theme.light' },
{ value: 'dark' as const, icon: RiMoonLine, label: 'theme.dark' },
{ value: 'system' as const, icon: RiComputerLine, label: 'theme.system' }
];
const languageOptions = [
{ value: 'en', label: 'English' },
{ value: 'zh-Hans', label: '简体中文' },
{ value: 'zh-Hant', label: '繁體中文' }
];
type LanguageMap = {
'en': 'zh-Hans';
'zh-Hans': 'zh-Hant';
'zh-Hant': 'en';
};
const languageMap: LanguageMap = {
'en': 'zh-Hans',
'zh-Hans': 'zh-Hant',
'zh-Hant': 'en'
};
const AdminLayout: FC<AdminLayoutProps> = () => {
const location = useLocation();
const { t, i18n } = useTranslation();
const { theme, setTheme } = useTheme();
return (
<div className="min-h-screen bg-slate-100 dark:bg-slate-900 py-6 flex">
{/* Background Overlay */}
<div className="fixed inset-0 bg-gradient-to-br from-slate-200 to-slate-300 dark:from-slate-800 dark:to-slate-900 backdrop-blur-xl -z-10" />
<div className="w-full max-w-[98%] mx-auto flex gap-4">
{/* Sidebar */}
<aside className="w-64 bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg rounded-lg shadow-lg flex flex-col">
<div className="h-16 px-6 border-b border-slate-200/80 dark:border-slate-700/80 flex items-center justify-center">
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">
TSS Rocks
</h1>
</div>
<nav className="flex-1 p-4">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-3 px-4 py-3 mb-2 rounded-lg transition-colors ${
isActive
? 'bg-slate-900 dark:bg-slate-700 text-white'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700/50'
}`}
>
<Icon className="text-xl" />
<span className="tracking-wide">{t(item.label)}</span>
</Link>
);
})}
</nav>
<div className="p-4 space-y-2">
{/* Language and Logout Buttons */}
<div className="flex gap-2">
<button
onClick={() => {
const currentLang = i18n.language as keyof LanguageMap;
const nextLang = languageMap[currentLang] || 'en';
i18n.changeLanguage(nextLang);
}}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors whitespace-nowrap min-w-[100px]"
title={t('admin.common.switchLanguage')}
>
<RiGlobalLine className="text-xl flex-shrink-0" />
<span className="text-sm truncate">{languageOptions.find(lang => lang.value === i18n.language)?.label || 'English'}</span>
</button>
<button
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors whitespace-nowrap"
onClick={() => {/* TODO: Implement logout */}}
>
<RiLogoutBoxRLine className="text-xl flex-shrink-0" />
<span className="text-sm">{t('admin.common.logout')}</span>
</button>
</div>
{/* Theme Buttons */}
<div className="border-t border-slate-200/80 dark:border-slate-700/80 pt-2">
<div className="flex items-center gap-1">
{themeOptions.map((option) => {
const Icon = option.icon;
return (
<button
key={option.value}
onClick={() => setTheme(option.value)}
className={`flex-1 p-2 rounded-md transition-colors ${
theme === option.value
? 'bg-slate-900 dark:bg-slate-700 text-white'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700/50'
}`}
title={t(option.label)}
>
<Icon className="text-xl mx-auto" />
</button>
);
})}
</div>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col rounded-lg overflow-hidden bg-white/80 dark:bg-slate-800/80 backdrop-blur-lg shadow-lg">
<header className="border-b border-slate-200/80 dark:border-slate-700/80">
<div className="h-16 px-8 flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">
{t(menuItems.find(item => item.path === location.pathname)?.label || 'admin.nav.dashboard')}
</h2>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-slate-200 dark:bg-slate-700 rounded-full flex items-center justify-center text-slate-600 dark:text-slate-300">
<span className="text-lg">A</span>
</div>
<div>
<div className="text-slate-800 dark:text-white"></div>
<div className="text-sm text-slate-500 dark:text-slate-400">Administrator</div>
</div>
</div>
</div>
</header>
<div className="flex-1 p-6">
<div className="h-full bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-slate-200/60 dark:border-slate-700/60">
<Outlet />
</div>
</div>
</main>
</div>
</div>
);
};
export default AdminLayout;