[feature/frontend] admin panel (wip)
This commit is contained in:
parent
34ebb05808
commit
1526c27b49
18 changed files with 1130 additions and 22 deletions
168
frontend/src/pages/admin/layout/AdminLayout.tsx
Normal file
168
frontend/src/pages/admin/layout/AdminLayout.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue