[feature/frontend] create posts (wip)

This commit is contained in:
CDN 2025-02-22 03:46:57 +08:00
parent e86d8c1576
commit 086c9761a9
Signed by: CDN
GPG key ID: 0C656827F9F80080
10 changed files with 598 additions and 115 deletions

View file

@ -1,6 +1,6 @@
{
"categories": {
"man": "Man",
"man": "Human",
"machine": "Machine",
"earth": "Earth",
"space": "Space",
@ -29,8 +29,6 @@
"edit": "Edit",
"delete": "Delete",
"upload": "Upload",
"save": "Save",
"saving": "Saving...",
"status": "Status",
"actions": "Actions",
"published": "Published",
@ -48,12 +46,10 @@
"joinDate": "Join Date",
"username": "Username",
"logout": "Logout",
"language": "Language",
"theme": {
"light": "Light Mode",
"dark": "Dark Mode",
"system": "System"
}
"save": "Save",
"saving": "Saving...",
"noData": "No data available",
"unsavedChanges": "You have unsaved changes. Are you sure you want to leave?"
},
"dashboard": {
"totalPosts": "Total Posts",
@ -61,13 +57,33 @@
"totalUsers": "Total Users",
"totalContributors": "Total Contributors"
},
"posts": {
"title": "Title",
"categories": "Categories",
"createdAt": "Created At",
"status": "Status",
"noTitle": "No Title",
"deleteConfirm": "Are you sure you want to delete this post?",
"create": "Create Post",
"edit": "Edit Post",
"slug": "Slug",
"content": "Content",
"summary": "Summary",
"metaKeywords": "Meta Keywords",
"metaDescription": "Meta Description",
"selectCategories": "Select Categories",
"saving": "Saving...",
"publishing": "Publishing...",
"saveDraft": "Save Draft",
"publish": "Publish"
},
"login": {
"title": "Admin Login",
"username": "Username",
"password": "Password",
"remember": "Remember me",
"submit": "Sign in",
"loading": "Signing in...",
"submit": "Login",
"loading": "Logging in...",
"error": {
"failed": "Login failed",
"retry": "Login failed, please try again later"
@ -76,7 +92,7 @@
"nav": {
"dashboard": "Dashboard",
"posts": "Posts",
"daily": "Daily Quotes",
"daily": "Daily",
"medias": "Media",
"categories": "Categories",
"users": "Users",

View file

@ -47,7 +47,9 @@
"username": "用户名",
"logout": "退出登录",
"save": "保存",
"saving": "保存中..."
"saving": "保存中...",
"noData": "暂无数据",
"unsavedChanges": "你有未保存的更改,确定要离开吗?"
},
"dashboard": {
"totalPosts": "文章总数",
@ -55,6 +57,26 @@
"totalUsers": "用户总数",
"totalContributors": "贡献者总数"
},
"posts": {
"title": "标题",
"categories": "分类",
"createdAt": "创建时间",
"status": "状态",
"noTitle": "无标题",
"deleteConfirm": "确定要删除这篇文章吗?",
"create": "创建文章",
"edit": "编辑文章",
"slug": "文章链接",
"content": "内容",
"summary": "摘要",
"metaKeywords": "关键词",
"metaDescription": "描述",
"selectCategories": "选择分类",
"saving": "保存中...",
"publishing": "发布中...",
"saveDraft": "保存草稿",
"publish": "发布文章"
},
"login": {
"title": "管理员登录",
"username": "用户名",

View file

@ -45,7 +45,7 @@
"lastLogin": "最後登入",
"joinDate": "加入時間",
"username": "用戶名",
"logout": "退出登",
"logout": "退出登",
"language": "語言",
"theme": {
"light": "淺色模式",
@ -53,7 +53,9 @@
"system": "跟隨系統"
},
"save": "保存",
"saving": "保存中..."
"saving": "保存中...",
"noData": "暫無數據",
"unsavedChanges": "你有未保存的更改,確定要離開嗎?"
},
"nav": {
"dashboard": "儀表板",
@ -83,15 +85,15 @@
"uploadDate": "上傳日期"
},
"login": {
"title": "管理員登",
"title": "管理員登",
"username": "用戶名",
"password": "密碼",
"remember": "記住我",
"submit": "登",
"loading": "登中...",
"submit": "登",
"loading": "登中...",
"error": {
"failed": "登失敗",
"retry": "登失敗,請稍後重試"
"failed": "登失敗",
"retry": "登失敗,請稍後重試"
}
},
"roles": {

View file

@ -12,7 +12,10 @@
"dependencies": {
"@headlessui/react": "^2.2.0",
"@tss-rocks/api": "workspace:*",
"@types/classnames": "^2.3.4",
"@types/markdown-it": "^14.1.2",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.4",
"lucide-react": "^0.474.0",

View file

@ -0,0 +1,26 @@
import { createContext, useContext, FC, ReactNode, useState } from 'react';
interface PageTitleContextType {
title: string;
setTitle: (title: string) => void;
}
const PageTitleContext = createContext<PageTitleContextType | undefined>(undefined);
export const PageTitleProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [title, setTitle] = useState('');
return (
<PageTitleContext.Provider value={{ title, setTitle }}>
{children}
</PageTitleContext.Provider>
);
};
export const usePageTitle = () => {
const context = useContext(PageTitleContext);
if (context === undefined) {
throw new Error('usePageTitle must be used within a PageTitleProvider');
}
return context;
};

View file

@ -19,6 +19,7 @@ import { useTheme } from '../../../hooks/useTheme';
import { Suspense } from 'react';
import LoadingSpinner from '../../../components/LoadingSpinner';
import { useUser } from '../../../contexts/UserContext';
import { PageTitleProvider, usePageTitle } from '../../../contexts/PageTitleContext';
interface AdminLayoutProps {}
@ -56,12 +57,13 @@ const languageMap: LanguageMap = {
'zh-Hant': 'en'
};
const AdminLayout: FC<AdminLayoutProps> = () => {
const AdminLayoutContent: FC = () => {
const { t, i18n } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const { theme, setTheme } = useTheme();
const { user, loading, error, fetchUser } = useUser();
const { title } = usePageTitle();
useEffect(() => {
// 如果没有 token重定向到登录页
@ -196,7 +198,7 @@ const AdminLayout: FC<AdminLayoutProps> = () => {
<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')}
{title || t(menuItems.find(item => item.path === location.pathname)?.label || 'admin.nav.dashboard')}
</h2>
</div>
<div className="flex items-center gap-3">
@ -224,4 +226,12 @@ const AdminLayout: FC<AdminLayoutProps> = () => {
);
};
const AdminLayout: FC<AdminLayoutProps> = () => {
return (
<PageTitleProvider>
<AdminLayoutContent />
</PageTitleProvider>
);
};
export default AdminLayout;

View file

@ -0,0 +1,296 @@
import { useState, useEffect, FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams, useBlocker } from 'react-router-dom';
import LoadingSpinner from '../../../components/LoadingSpinner';
import { usePageTitle } from '../../../contexts/PageTitleContext';
import classNames from 'classnames';
interface PostContent {
language_code: 'en' | 'zh-Hans' | 'zh-Hant';
title: string;
content_markdown: string;
summary?: string;
meta_keywords?: string;
meta_description?: string;
}
interface Category {
id: number;
contents: Array<{
language_code: string;
name: string;
slug: string;
description?: string;
}>;
}
interface Post {
id?: number;
slug: string;
status: 'draft' | 'published';
contents: PostContent[];
categories: Category[];
created_at?: string;
updated_at?: string;
}
const LANGUAGES = [
{ code: 'en', label: 'English' },
{ code: 'zh-Hans', label: '简体中文' },
{ code: 'zh-Hant', label: '繁體中文' }
] as const;
const PostEditor: FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { postId } = useParams<{ postId: string }>();
const isEditing = !!postId;
const { setTitle } = usePageTitle();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [activeTab, setActiveTab] = useState<'en' | 'zh-Hans' | 'zh-Hant'>('en');
const [post, setPost] = useState<Post>({
slug: '',
status: 'draft',
contents: [
{
language_code: 'en',
title: '',
content_markdown: ''
},
{
language_code: 'zh-Hans',
title: '',
content_markdown: ''
},
{
language_code: 'zh-Hant',
title: '',
content_markdown: ''
}
],
categories: []
});
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
isDirty && currentLocation.pathname !== nextLocation.pathname
);
useEffect(() => {
if (blocker.state === 'blocked') {
const confirmed = window.confirm(t('admin.common.unsavedChanges'));
if (confirmed) {
blocker.proceed();
} else {
blocker.reset();
}
}
}, [blocker, t]);
useEffect(() => {
setTitle(isEditing ? t('admin.posts.edit') : t('admin.posts.create'));
}, [isEditing, t, setTitle]);
useEffect(() => {
if (isEditing) {
fetchPost();
}
}, [postId]);
const fetchPost = async () => {
try {
setLoading(true);
const response = await fetch(`/api/v1/posts/${postId}`);
if (!response.ok) throw new Error('Failed to fetch post');
const data = await response.json();
setPost(data.data);
setIsDirty(false);
} catch (error) {
console.error('Error fetching post:', error);
} finally {
setLoading(false);
}
};
const handleSubmit = async (publish: boolean = false) => {
try {
setSaving(true);
const method = isEditing ? 'PUT' : 'POST';
const url = isEditing ? `/api/v1/posts/${postId}` : '/api/v1/posts';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...post,
status: publish ? 'published' : 'draft'
}),
});
if (!response.ok) throw new Error('Failed to save post');
setIsDirty(false);
navigate('/admin/posts');
} catch (error) {
console.error('Error saving post:', error);
} finally {
setSaving(false);
}
};
const activeContent = post.contents.find(c => c.language_code === activeTab)!;
const updateContent = (updates: Partial<PostContent>) => {
setIsDirty(true);
const newContents = post.contents.map(content =>
content.language_code === activeTab
? { ...content, ...updates }
: content
);
setPost(prev => ({ ...prev, contents: newContents }));
};
const handleSlugChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsDirty(true);
setPost(prev => ({ ...prev, slug: e.target.value }));
};
if (loading) {
return <LoadingSpinner />;
}
return (
<div className="p-6">
<div className="space-y-6">
{/* Slug */}
<div>
<label className="block text-sm font-medium mb-2 text-slate-900 dark:text-white">
{t('admin.posts.slug')}
</label>
<input
type="text"
value={post.slug}
onChange={handleSlugChange}
className="w-full px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400"
/>
</div>
{/* Language Tabs */}
<div className="border-b border-slate-200 dark:border-slate-700">
<nav className="-mb-px flex space-x-8" aria-label="Language">
{LANGUAGES.map(({ code, label }) => (
<button
key={code}
onClick={() => setActiveTab(code)}
className={`whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === code
? 'border-slate-900 dark:border-white text-slate-900 dark:text-white'
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 hover:border-slate-300 dark:hover:border-slate-600'
}`}
>
{label}
</button>
))}
</nav>
</div>
{/* Content Fields */}
<div className="space-y-4">
{/* Title */}
<div>
<label className="block text-sm font-medium mb-2 text-slate-900 dark:text-white">
{t('admin.posts.title')}
</label>
<input
type="text"
value={activeContent.title}
onChange={e => updateContent({ title: e.target.value })}
className="w-full px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400"
/>
</div>
{/* Content */}
<div>
<label className="block text-sm font-medium mb-2 text-slate-900 dark:text-white">
{t('admin.posts.content')}
</label>
<textarea
value={activeContent.content_markdown}
onChange={e => updateContent({ content_markdown: e.target.value })}
rows={10}
className="w-full px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400"
/>
</div>
{/* Summary */}
<div>
<label className="block text-sm font-medium mb-2 text-slate-900 dark:text-white">
{t('admin.posts.summary')}
</label>
<textarea
value={activeContent.summary || ''}
onChange={e => updateContent({ summary: e.target.value })}
rows={3}
className="w-full px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400"
/>
</div>
{/* Meta Keywords */}
<div>
<label className="block text-sm font-medium mb-2 text-slate-900 dark:text-white">
{t('admin.posts.metaKeywords')}
</label>
<input
type="text"
value={activeContent.meta_keywords || ''}
onChange={e => updateContent({ meta_keywords: e.target.value })}
className="w-full px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400"
/>
</div>
{/* Meta Description */}
<div>
<label className="block text-sm font-medium mb-2 text-slate-900 dark:text-white">
{t('admin.posts.metaDescription')}
</label>
<textarea
value={activeContent.meta_description || ''}
onChange={e => updateContent({ meta_description: e.target.value })}
rows={2}
className="w-full px-3 py-2 bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400"
/>
</div>
</div>
{/* Categories */}
{/* TODO: Add category selection */}
{/* Actions */}
<div className="flex justify-end space-x-4">
<button
type="button"
onClick={() => handleSubmit(false)}
disabled={saving}
className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-md hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400 disabled:opacity-50"
>
{saving ? t('admin.posts.saving') : t('admin.posts.saveDraft')}
</button>
<button
type="button"
onClick={() => handleSubmit(true)}
disabled={saving}
className="px-4 py-2 text-sm font-medium text-white bg-slate-900 dark:bg-slate-700 rounded-md hover:bg-slate-800 dark:hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-slate-500 dark:focus:ring-slate-400 disabled:opacity-50"
>
{saving ? t('admin.posts.publishing') : t('admin.posts.publish')}
</button>
</div>
</div>
</div>
);
};
export default PostEditor;

View file

@ -1,73 +1,183 @@
import { useState } from 'react';
import { useState, useEffect, FC, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import Table from '../../../components/admin/Table';
import TableActions from '../../../components/admin/TableActions';
import { RiAddLine, RiSearchLine } from 'react-icons/ri';
import { RiSearchLine } from 'react-icons/ri';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/zh-cn';
import { useNavigate } from 'react-router-dom';
import { usePageTitle } from '../../../contexts/PageTitleContext';
// 初始化 dayjs 插件
dayjs.extend(relativeTime);
dayjs.locale('zh-cn');
interface PostContent {
language_code: 'en' | 'zh-Hans' | 'zh-Hant';
title: string;
content_markdown: string;
summary?: string;
meta_keywords?: string;
meta_description?: string;
}
interface Category {
id: number;
contents: Array<{
language_code: string;
name: string;
slug: string;
description?: string;
}>;
}
interface Post {
id: string;
title: string;
category: string;
publishDate: string;
id: number;
slug: string;
status: 'draft' | 'published';
contents: PostContent[];
categories: Category[];
created_at: string;
updated_at: string;
}
type TablePost = {
id: number;
slug: string;
status: 'draft' | 'published';
created_at: string;
updated_at: string;
displayTitle: string;
displayCategories: string;
};
const PostsManagement: FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [posts, setPosts] = useState<TablePost[]>([]);
const { t } = useTranslation();
const navigate = useNavigate();
const { setTitle } = usePageTitle();
// 这里后续会通过 API 获取数据
const posts: Post[] = [
{
id: '1',
title: '示例文章标题',
category: '示例分类',
publishDate: '2024-02-20',
status: 'published',
},
];
useEffect(() => {
setTitle(t('admin.nav.posts'));
return () => setTitle('');
}, [setTitle, t]);
const handleEdit = (post: Post) => {
console.log('Edit post:', post);
useEffect(() => {
fetchPosts();
}, []);
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch('/api/v1/posts?sort=-created_at');
if (!response.ok) throw new Error('Failed to fetch posts');
const data = await response.json();
// 处理数据,添加显示字段
const processedPosts = data.data.map((post: Post): TablePost => ({
id: post.id,
slug: post.slug,
status: post.status,
created_at: post.created_at,
updated_at: post.updated_at,
displayTitle: getPostTitle(post),
displayCategories: getCategoryNames(post)
}));
setPosts(processedPosts);
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
};
const handleDelete = (post: Post) => {
console.log('Delete post:', post);
const getPostTitle = (post: Post) => {
// Try to get English title first
const englishContent = post.contents.find(c => c.language_code === 'en');
if (englishContent?.title) return englishContent.title;
// Fallback to the first available title
const firstContent = post.contents[0];
return firstContent?.title || t('admin.posts.noTitle');
};
const getCategoryNames = (post: Post) => {
return post.categories
.map(category => {
const englishContent = category.contents.find(c => c.language_code === 'en');
if (englishContent?.name) return englishContent.name;
return category.contents[0]?.name || '';
})
.filter(Boolean)
.join(', ');
};
const handleEdit = async (post: TablePost) => {
navigate(post.slug);
};
const handleDelete = async (post: TablePost) => {
if (!window.confirm(t('admin.posts.deleteConfirm'))) return;
try {
const response = await fetch(`/api/v1/posts/${post.slug}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete post');
await fetchPosts();
} catch (error) {
console.error('Error deleting post:', error);
}
};
const columns = [
{
key: 'title' as keyof Post,
key: 'displayTitle' as keyof TablePost,
title: t('admin.posts.title'),
render: (value: string): ReactNode => value
},
{
key: 'category' as keyof Post,
title: t('admin.posts.category'),
key: 'displayCategories' as keyof TablePost,
title: t('admin.posts.categories'),
render: (value: string): ReactNode => value
},
{
key: 'publishDate' as keyof Post,
title: t('admin.posts.publishDate'),
},
{
key: 'status' as keyof Post,
title: t('admin.posts.status'),
render: (value: Post['status']) => (
<span
className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${
value === 'published'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
}`}
>
{t(`admin.common.${value}`)}
key: 'created_at' as keyof TablePost,
title: t('admin.posts.createdAt'),
render: (value: string): ReactNode => (
<span title={dayjs(value).format('YYYY-MM-DD HH:mm:ss')}>
{dayjs(value).fromNow()}
</span>
),
)
},
{
key: 'status' as keyof TablePost,
title: t('admin.posts.status'),
render: (value: string | number): ReactNode => {
const status = value as TablePost['status'];
return (
<span
className={`inline-block px-2 py-1 text-xs font-medium rounded-full ${
status === 'published'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
}`}
>
{t(`admin.common.${status}`)}
</span>
);
}
}
];
const handleCreate = () => {
// TODO: 实现创建文章的逻辑
navigate('new');
};
return (
@ -89,59 +199,13 @@ const PostsManagement: FC = () => {
</div>
<div>
<Table<Post>
<Table<TablePost>
columns={columns}
data={posts}
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
>
{({ data, onEdit, onDelete }) => (
<table className="w-full">
<thead>
<tr className="border-b border-slate-200 dark:border-slate-700">
<th className="text-left py-4 px-6 text-slate-600 dark:text-slate-400 font-medium rounded-tl-md">{t('admin.common.title')}</th>
<th className="text-left py-4 px-6 text-slate-600 dark:text-slate-400 font-medium">{t('admin.common.category')}</th>
<th className="text-left py-4 px-6 text-slate-600 dark:text-slate-400 font-medium">{t('admin.common.publishDate')}</th>
<th className="text-left py-4 px-6 text-slate-600 dark:text-slate-400 font-medium">{t('admin.common.status')}</th>
<th className="text-right py-4 px-6 text-slate-600 dark:text-slate-400 font-medium rounded-tr-md">{t('admin.common.actions')}</th>
</tr>
</thead>
<tbody>
{data.map((post) => (
<tr key={post.id} className="border-b border-slate-200 dark:border-slate-700 last:border-0">
<td className="py-4 px-6 text-slate-800 dark:text-slate-200">{post.title}</td>
<td className="py-4 px-6 text-slate-800 dark:text-slate-200">{post.category}</td>
<td className="py-4 px-6 text-slate-800 dark:text-slate-200">{post.publishDate}</td>
<td className="py-4 px-6">
<span className={`px-2 py-1 text-sm font-medium rounded-full ${
post.status === 'published'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
}`}>
{t(`admin.common.${post.status}`)}
</span>
</td>
<td className="py-4 px-6 text-right">
<button
className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 px-2 py-1 rounded-md transition-colors"
onClick={() => onEdit(post)}
>
{t('admin.common.edit')}
</button>
<button
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"
onClick={() => onDelete(post)}
>
{t('admin.common.delete')}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</Table>
/>
</div>
</div>
);

View file

@ -14,6 +14,7 @@ const Footer = lazy(() => import('./components/Footer'));
// 管理页面组件
const Dashboard = lazy(() => import('./pages/admin/dashboard/Dashboard'));
const PostsManagement = lazy(() => import('./pages/admin/posts/PostsManagement'));
const PostEditor = lazy(() => import('./pages/admin/posts/PostEditor'));
const DailyManagement = lazy(() => import('./pages/admin/daily/DailyManagement'));
const MediasManagement = lazy(() => import('./pages/admin/medias/MediasManagement'));
const CategoriesManagement = lazy(() => import('./pages/admin/categories/CategoriesManagement'));
@ -140,11 +141,32 @@ const router = createBrowserRouter([
},
{
path: 'posts',
element: (
<Suspense fallback={<LoadingSpinner fullScreen />}>
<PostsManagement />
</Suspense>
),
children: [
{
index: true,
element: (
<Suspense fallback={<LoadingSpinner fullScreen />}>
<PostsManagement />
</Suspense>
),
},
{
path: 'new',
element: (
<Suspense fallback={<LoadingSpinner fullScreen />}>
<PostEditor />
</Suspense>
),
},
{
path: ':postId',
element: (
<Suspense fallback={<LoadingSpinner fullScreen />}>
<PostEditor />
</Suspense>
),
},
],
},
{
path: 'daily',

22
pnpm-lock.yaml generated
View file

@ -22,9 +22,18 @@ importers:
'@tss-rocks/api':
specifier: workspace:*
version: link:../api
'@types/classnames':
specifier: ^2.3.4
version: 2.3.4
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
classnames:
specifier: ^2.5.1
version: 2.5.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
i18next:
specifier: ^24.2.2
version: 24.2.2(typescript@5.7.3)
@ -843,6 +852,10 @@ packages:
'@types/babel__traverse@7.20.6':
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
'@types/classnames@2.3.4':
resolution: {integrity: sha512-dwmfrMMQb9ujX1uYGvB5ERDlOzBNywnZAZBtOe107/hORWP05ESgU4QyaanZMWYYfd2BzrG78y13/Bju8IQcMQ==}
deprecated: This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@ -1080,6 +1093,9 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
@ -2898,6 +2914,10 @@ snapshots:
dependencies:
'@babel/types': 7.26.9
'@types/classnames@2.3.4':
dependencies:
classnames: 2.5.1
'@types/cookie@0.6.0': {}
'@types/estree@1.0.6': {}
@ -3164,6 +3184,8 @@ snapshots:
csstype@3.1.3: {}
dayjs@1.11.13: {}
debug@4.4.0:
dependencies:
ms: 2.1.3