diff --git a/frontend/data/i18n/en.json b/frontend/data/i18n/en.json index 5e8d59a..defb85f 100644 --- a/frontend/data/i18n/en.json +++ b/frontend/data/i18n/en.json @@ -20,5 +20,42 @@ }, "footer": { "copyright": "TSS Rocks. All rights reserved." + }, + "admin": { + "common": { + "search": "Search", + "create": "Create", + "edit": "Edit", + "delete": "Delete", + "status": "Status", + "actions": "Actions", + "published": "Published", + "draft": "Draft", + "author": "Author", + "date": "Date", + "title": "Title", + "description": "Description", + "name": "Name", + "email": "Email", + "role": "Role", + "bio": "Bio", + "articles": "Articles", + "lastLogin": "Last Login", + "joinDate": "Join Date", + "username": "Username", + "logout": "Logout" + }, + "nav": { + "dashboard": "Dashboard", + "posts": "Posts", + "categories": "Categories", + "users": "Users", + "contributors": "Contributors" + }, + "roles": { + "admin": "Administrator", + "user": "User", + "contributor": "Contributor" + } } } diff --git a/frontend/data/i18n/zh-Hans.json b/frontend/data/i18n/zh-Hans.json index b1e2848..749c864 100644 --- a/frontend/data/i18n/zh-Hans.json +++ b/frontend/data/i18n/zh-Hans.json @@ -19,6 +19,43 @@ "system": "跟随系统" }, "footer": { - "copyright": "TSS.Rocks. 版权所有。" + "copyright": "TSS Rocks. 保留所有权利。" + }, + "admin": { + "common": { + "search": "搜索", + "create": "新建", + "edit": "编辑", + "delete": "删除", + "status": "状态", + "actions": "操作", + "published": "已发布", + "draft": "草稿", + "author": "作者", + "date": "日期", + "title": "标题", + "description": "描述", + "name": "名称", + "email": "邮箱", + "role": "角色", + "bio": "简介", + "articles": "文章数", + "lastLogin": "最后登录", + "joinDate": "加入时间", + "username": "用户名", + "logout": "退出" + }, + "nav": { + "dashboard": "仪表盘", + "posts": "文章", + "categories": "分类", + "users": "用户", + "contributors": "作者" + }, + "roles": { + "admin": "管理员", + "user": "普通用户", + "contributor": "作者" + } } } diff --git a/frontend/data/i18n/zh-Hant.json b/frontend/data/i18n/zh-Hant.json index 988497f..6377964 100644 --- a/frontend/data/i18n/zh-Hant.json +++ b/frontend/data/i18n/zh-Hant.json @@ -19,6 +19,43 @@ "system": "跟隨系統" }, "footer": { - "copyright": "TSS.Rocks. 版權所有。" + "copyright": "TSS Rocks. 保留所有權利。" + }, + "admin": { + "common": { + "search": "搜尋", + "create": "新建", + "edit": "編輯", + "delete": "刪除", + "status": "狀態", + "actions": "操作", + "published": "已發布", + "draft": "草稿", + "author": "作者", + "date": "日期", + "title": "標題", + "description": "描述", + "name": "名稱", + "email": "郵箱", + "role": "角色", + "bio": "簡介", + "articles": "文章數", + "lastLogin": "最後登入", + "joinDate": "加入時間", + "username": "用戶名", + "logout": "退出" + }, + "nav": { + "dashboard": "儀表板", + "posts": "文章", + "categories": "分類", + "users": "用戶", + "contributors": "作者" + }, + "roles": { + "admin": "管理員", + "user": "普通用戶", + "contributor": "作者" + } } } diff --git a/frontend/package.json b/frontend/package.json index 0ad7daa..ff1ee5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,11 @@ "preview": "vite preview" }, "dependencies": { - "@tss-rocks/api": "workspace:*", "@headlessui/react": "^2.2.0", + "@tss-rocks/api": "workspace:*", "@types/markdown-it": "^14.1.2", "i18next": "^24.2.2", + "i18next-browser-languagedetector": "^8.0.4", "lucide-react": "^0.474.0", "markdown-it": "^14.1.0", "react": "^19.0.0", @@ -27,6 +28,7 @@ "@tailwindcss/postcss": "^4.0.3", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.0.3", + "@types/node": "^22.13.4", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e7885dd..c5f2ca5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,35 +3,61 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Header } from './components/layout/Header'; import { Suspense, lazy } from 'react'; import Footer from './components/Footer'; +import { AuthProvider } from './contexts/AuthContext'; // Lazy load pages const Home = lazy(() => import('./pages/Home')); const Daily = lazy(() => import('./pages/Daily')); const Article = lazy(() => import('./pages/Article')); +const AdminLayout = lazy(() => import('./pages/admin/layout/AdminLayout')); + +// 管理页面懒加载 +const PostsManagement = lazy(() => import('./pages/admin/posts/PostsManagement')); +const CategoriesManagement = lazy(() => import('./pages/admin/categories/CategoriesManagement')); +const UsersManagement = lazy(() => import('./pages/admin/users/UsersManagement')); +const ContributorsManagement = lazy(() => import('./pages/admin/contributors/ContributorsManagement')); function App() { return ( - - - - {/* 页眉分隔线 */} - - - - - + + + Loading...}> - } /> - } /> - } /> + {/* Admin routes */} + }> + } /> + } /> + } /> + } /> + + + {/* Public routes */} + + + {/* 页眉分隔线 */} + + + + + + } /> + } /> + } /> + + + + > + } + /> - - - - - + + + ); } diff --git a/frontend/src/components/admin/Table.tsx b/frontend/src/components/admin/Table.tsx new file mode 100644 index 0000000..d7a6b8a --- /dev/null +++ b/frontend/src/components/admin/Table.tsx @@ -0,0 +1,132 @@ +import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface Column { + key: keyof T; + title: string; + render?: (value: T[keyof T], item: T) => ReactNode; +} + +interface TableProps { + columns: Column[]; + data: T[]; + loading?: boolean; + onEdit?: (item: T) => void; + onDelete?: (item: T) => void; +} + +const Table = >({ + columns, + data, + loading = false, + onEdit, + onDelete, +}: TableProps) => { + const { t } = useTranslation(); + + if (loading) { + return ( + + {/* Loading header */} + + {columns.map((_, index) => ( + + ))} + + {/* Loading rows */} + + {[1, 2, 3].map((row) => ( + + {columns.map((_, index) => ( + + + + ))} + + ))} + + + ); + } + + return ( + + + + + {columns.map((column) => ( + + {column.title} + + ))} + {(onEdit || onDelete) && ( + + {t('admin.common.actions')} + + )} + + + + {data.map((item, itemIdx) => ( + + {columns.map((column) => ( + + {column.render + ? column.render(item[column.key], item) + : String(item[column.key])} + + ))} + {(onEdit || onDelete) && ( + + {onEdit && ( + onEdit(item)} + className="text-slate-600 dark:text-white hover:text-slate-900 dark:hover:text-slate-200 px-2 py-1 rounded-md transition-colors" + > + {t('admin.common.edit')} + + )} + {onDelete && ( + onDelete(item)} + className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 px-2 py-1 rounded-md transition-colors" + > + {t('admin.common.delete')} + + )} + + )} + + ))} + + + {data.length === 0 && !loading && ( + + {t('admin.common.noData')} + + )} + + ); +}; + +export default Table; diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index ece24f6..abbf3b1 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { FiSun, FiMoon, FiSearch, FiGlobe, FiMonitor } from 'react-icons/fi'; import { Menu } from '@headlessui/react'; import { useTheme } from '../../hooks/useTheme'; +import type { Theme } from '../../hooks/useTheme'; const LANGUAGES = [ { code: 'en', nativeName: 'English' }, diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..ccf510b --- /dev/null +++ b/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,82 @@ +import { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import { AuthContextType, AuthState, User } from '../types/auth'; + +const API_URL = import.meta.env.VITE_API_URL || '/api'; + +const initialState: AuthState = { + user: null, + token: null, + loading: false, + error: null, +}; + +const AuthContext = createContext(null); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const [state, setState] = useState(initialState); + + const login = useCallback(async (email: string, password: string) => { + setState(prev => ({ ...prev, loading: true, error: null })); + try { + const response = await fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + throw new Error('登录失败'); + } + + const data = await response.json(); + setState(prev => ({ + ...prev, + user: data.user, + token: data.token, + loading: false, + })); + + localStorage.setItem('token', data.token); + } catch (error) { + setState(prev => ({ + ...prev, + loading: false, + error: error instanceof Error ? error.message : '登录失败', + })); + } + }, []); + + const logout = useCallback(() => { + localStorage.removeItem('token'); + setState(initialState); + }, []); + + const clearError = useCallback(() => { + setState(prev => ({ ...prev, error: null })); + }, []); + + const value = { + ...state, + login, + logout, + clearError, + }; + + return {children}; +}; + +export default AuthContext; diff --git a/frontend/src/hooks/useTheme.ts b/frontend/src/hooks/useTheme.ts index ce2910b..d77509a 100644 --- a/frontend/src/hooks/useTheme.ts +++ b/frontend/src/hooks/useTheme.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -type Theme = 'light' | 'dark' | 'system'; +export type Theme = 'light' | 'dark' | 'system'; export function useTheme() { const [theme, setTheme] = useState( diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 1dc336a..ac84854 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -1,11 +1,27 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; import en from '../data/i18n/en.json'; import zhHans from '../data/i18n/zh-Hans.json'; import zhHant from '../data/i18n/zh-Hant.json'; +// 获取浏览器默认语言 +const getBrowserLanguage = () => { + const lang = navigator.language; + if (lang.startsWith('zh')) { + return lang === 'zh-TW' || lang === 'zh-HK' ? 'zh-Hant' : 'zh-Hans'; + } + return 'en'; +}; + +// 获取存储的语言设置 +const getStoredLanguage = () => { + return localStorage.getItem('language') || getBrowserLanguage(); +}; + i18n + .use(LanguageDetector) .use(initReactI18next) .init({ resources: { @@ -13,11 +29,21 @@ i18n 'zh-Hans': { translation: zhHans }, 'zh-Hant': { translation: zhHant }, }, - lng: 'zh-Hans', + lng: getStoredLanguage(), fallbackLng: 'en', + detection: { + order: ['localStorage', 'navigator'], + lookupLocalStorage: 'language', + caches: ['localStorage'], + }, interpolation: { escapeValue: false, }, }); +// 监听语言变化并保存到 localStorage +i18n.on('languageChanged', (lng) => { + localStorage.setItem('language', lng); +}); + export default i18n; diff --git a/frontend/src/pages/admin/categories/CategoriesManagement.tsx b/frontend/src/pages/admin/categories/CategoriesManagement.tsx new file mode 100644 index 0000000..cdd1db3 --- /dev/null +++ b/frontend/src/pages/admin/categories/CategoriesManagement.tsx @@ -0,0 +1,129 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RiAddLine, RiSearchLine } from 'react-icons/ri'; +import Table from '../../../components/admin/Table'; + +interface Category { + id: string; + name: string; + description: string; +} + +const CategoriesManagement: FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + + // 这里后续会通过 API 获取数据 + const categories: Category[] = [ + { + id: '1', + name: '新闻', + description: '新闻分类', + }, + ]; + + const handleEdit = (category: Category) => { + console.log('Edit category:', category); + }; + + const handleDelete = (category: Category) => { + console.log('Delete category:', category); + }; + + const columns = [ + { + key: 'name', + title: t('admin.common.name'), + }, + { + key: 'description', + title: t('admin.common.description'), + }, + ]; + + return ( + + + + + setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2 border-b-2 border-slate-200 dark:border-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-transparent outline-none w-64 text-slate-800 dark:text-white placeholder-slate-400 dark:placeholder-slate-500" + /> + + + + + {t('admin.common.create')} + + + + + + + {({ data, onEdit, onDelete }) => ( + + + + {columns.map((column, index) => ( + + {column.title} + + ))} + {t('admin.common.actions')} + + + + {data.map((category) => ( + + {columns.map((column) => ( + + {category[column.key]} + + ))} + + onEdit(category)} + > + {t('admin.common.edit')} + + onDelete(category)} + > + {t('admin.common.delete')} + + + + ))} + + + )} + + + + ); +}; + +export default CategoriesManagement; diff --git a/frontend/src/pages/admin/contributors/ContributorsManagement.tsx b/frontend/src/pages/admin/contributors/ContributorsManagement.tsx new file mode 100644 index 0000000..8ace271 --- /dev/null +++ b/frontend/src/pages/admin/contributors/ContributorsManagement.tsx @@ -0,0 +1,138 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RiAddLine, RiSearchLine } from 'react-icons/ri'; +import Table from '../../../components/admin/Table'; + +interface Contributor { + id: string; + name: string; + bio: string; + articles: number; + joinDate: string; +} + +const ContributorsManagement: FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + + // 这里后续会通过 API 获取数据 + const contributors: Contributor[] = [ + { + id: '1', + name: '李四', + bio: '这是李四的简介', + articles: 5, + joinDate: '2024-02-20', + }, + ]; + + const handleEdit = (contributor: Contributor) => { + console.log('Edit contributor:', contributor); + }; + + const handleDelete = (contributor: Contributor) => { + console.log('Delete contributor:', contributor); + }; + + const columns = [ + { + key: 'name', + title: t('admin.common.name'), + }, + { + key: 'bio', + title: t('admin.common.bio'), + }, + { + key: 'articles', + title: t('admin.common.articles'), + render: (value: number) => ( + + {value} + + ), + }, + { + key: 'joinDate', + title: t('admin.common.joinDate'), + }, + ]; + + return ( + + + + + setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2 border-b-2 border-slate-200 dark:border-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-transparent outline-none w-64 text-slate-800 dark:text-white placeholder-slate-400 dark:placeholder-slate-500" + /> + + + + + {t('admin.common.create')} + + + + + + + {({ data, onEdit, onDelete }) => ( + + + + {t('admin.common.name')} + {t('admin.common.bio')} + {t('admin.common.articles')} + {t('admin.common.joinDate')} + {t('admin.common.actions')} + + + + {data.map((contributor) => ( + + {contributor.name} + {contributor.bio} + + + {contributor.articles} + + + {contributor.joinDate} + + onEdit(contributor)} + > + {t('admin.common.edit')} + + onDelete(contributor)} + > + {t('admin.common.delete')} + + + + ))} + + + )} + + + + ); +}; + +export default ContributorsManagement; diff --git a/frontend/src/pages/admin/layout/AdminLayout.tsx b/frontend/src/pages/admin/layout/AdminLayout.tsx new file mode 100644 index 0000000..1503e8a --- /dev/null +++ b/frontend/src/pages/admin/layout/AdminLayout.tsx @@ -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 = () => { + const location = useLocation(); + const { t, i18n } = useTranslation(); + const { theme, setTheme } = useTheme(); + + return ( + + {/* Background Overlay */} + + + + {/* Sidebar */} + + + {/* Main Content */} + + + + + + {t(menuItems.find(item => item.path === location.pathname)?.label || 'admin.nav.dashboard')} + + + + + A + + + 管理员 + Administrator + + + + + + + + + + + + + ); +}; + +export default AdminLayout; diff --git a/frontend/src/pages/admin/posts/PostsManagement.tsx b/frontend/src/pages/admin/posts/PostsManagement.tsx new file mode 100644 index 0000000..8850435 --- /dev/null +++ b/frontend/src/pages/admin/posts/PostsManagement.tsx @@ -0,0 +1,148 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import Table from '../../../components/admin/Table'; +import { RiAddLine, RiSearchLine } from 'react-icons/ri'; + +interface Post { + id: string; + title: string; + author: string; + status: 'draft' | 'published'; + publishDate: string; +} + +const PostsManagement: FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + + // 这里后续会通过 API 获取数据 + const posts: Post[] = [ + { + id: '1', + title: '示例文章标题', + author: '张三', + status: 'published', + publishDate: '2024-02-20', + }, + ]; + + const handleEdit = (post: Post) => { + console.log('Edit post:', post); + }; + + const handleDelete = (post: Post) => { + console.log('Delete post:', post); + }; + + const columns = [ + { + key: 'title', + title: t('admin.common.title'), + }, + { + key: 'author', + title: t('admin.common.author'), + }, + { + key: 'status', + title: t('admin.common.status'), + render: (value: Post['status']) => ( + + {t(`admin.common.${value}`)} + + ), + }, + { + key: 'publishDate', + title: t('admin.common.date'), + }, + ]; + + return ( + + + + + setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2 border-b-2 border-slate-200 dark:border-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-transparent outline-none w-64 text-slate-800 dark:text-white placeholder-slate-400 dark:placeholder-slate-500" + /> + + + + + {t('admin.common.create')} + + + + + + + {({ data, onEdit, onDelete }) => ( + + + + {t('admin.common.title')} + {t('admin.common.author')} + {t('admin.common.status')} + {t('admin.common.date')} + {t('admin.common.actions')} + + + + {data.map((post) => ( + + {post.title} + {post.author} + + + {t(`admin.common.${post.status}`)} + + + {post.publishDate} + + onEdit(post)} + > + {t('admin.common.edit')} + + onDelete(post)} + > + {t('admin.common.delete')} + + + + ))} + + + )} + + + + ); +}; + +export default PostsManagement; diff --git a/frontend/src/pages/admin/users/UsersManagement.tsx b/frontend/src/pages/admin/users/UsersManagement.tsx new file mode 100644 index 0000000..f31ed9e --- /dev/null +++ b/frontend/src/pages/admin/users/UsersManagement.tsx @@ -0,0 +1,100 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RiAddLine, RiSearchLine } from 'react-icons/ri'; +import Table from '../../../components/admin/Table'; + +interface User { + id: string; + username: string; + email: string; + role: 'admin' | 'user'; + lastLogin: string; +} + +const UsersManagement: FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(true); + const { t } = useTranslation(); + + // 这里后续会通过 API 获取数据 + const users: User[] = [ + { + id: '1', + username: '张三', + email: 'zhangsan@example.com', + role: 'admin', + lastLogin: '2024-02-20', + }, + ]; + + const handleEdit = (user: User) => { + console.log('Edit user:', user); + }; + + const handleDelete = (user: User) => { + console.log('Delete user:', user); + }; + + const columns = [ + { + key: 'username', + title: t('admin.common.username'), + }, + { + key: 'role', + title: t('admin.common.role'), + render: (value: User['role']) => ( + + {t(`admin.roles.${value}`)} + + ), + }, + { + key: 'joinDate', + title: t('admin.common.joinDate'), + }, + { + key: 'lastLogin', + title: t('admin.common.lastLogin'), + }, + ]; + + return ( + + + + + setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2 border-b-2 border-slate-200 dark:border-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-transparent outline-none w-64 text-slate-800 dark:text-white placeholder-slate-400 dark:placeholder-slate-500" + /> + + + + + {t('admin.common.create')} + + + + + + + ); +}; + +export default UsersManagement; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx new file mode 100644 index 0000000..b424958 --- /dev/null +++ b/frontend/src/router.tsx @@ -0,0 +1,13 @@ +import { createBrowserRouter } from 'react-router-dom'; +import AdminLayout from './pages/admin/layout/AdminLayout'; +import Dashboard from './pages/admin/Dashboard'; + +const router = createBrowserRouter([ + { + path: '/admin', + element: , + }, + // Add more routes here as we develop them +]); + +export default router; diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts new file mode 100644 index 0000000..03788f2 --- /dev/null +++ b/frontend/src/types/auth.ts @@ -0,0 +1,19 @@ +export interface User { + id: string; + username: string; + email: string; + role: 'admin' | 'contributor' | 'user'; +} + +export interface AuthState { + user: User | null; + token: string | null; + loading: boolean; + error: string | null; +} + +export interface AuthContextType extends AuthState { + login: (email: string, password: string) => Promise; + logout: () => void; + clearError: () => void; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cce9a98..77569e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: i18next: specifier: ^24.2.2 version: 24.2.2(typescript@5.7.3) + i18next-browser-languagedetector: + specifier: ^8.0.4 + version: 8.0.4 lucide-react: specifier: ^0.474.0 version: 0.474.0(react@19.0.0) @@ -62,6 +65,9 @@ importers: '@tailwindcss/vite': specifier: ^4.0.3 version: 4.0.7(vite@6.1.1(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.1)) + '@types/node': + specifier: ^22.13.4 + version: 22.13.4 '@types/react': specifier: ^19.0.8 version: 19.0.10 @@ -1358,6 +1364,9 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + i18next-browser-languagedetector@8.0.4: + resolution: {integrity: sha512-f3frU3pIxD50/Tz20zx9TD9HobKYg47fmAETb117GKGPrhwcSSPJDoCposXlVycVebQ9GQohC3Efbpq7/nnJ5w==} + i18next@24.2.2: resolution: {integrity: sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==} peerDependencies: @@ -3466,6 +3475,10 @@ snapshots: transitivePeerDependencies: - supports-color + i18next-browser-languagedetector@8.0.4: + dependencies: + '@babel/runtime': 7.26.9 + i18next@24.2.2(typescript@5.7.3): dependencies: '@babel/runtime': 7.26.9
{t('admin.common.noData')}