feat: 重构前端界面并修复认证路径
- 重构主页、关于页、演示页和帖子页的UI,使用现代化的卡片设计和布局 - 添加主题切换功能,支持亮色/暗色模式 - 修复后端认证路由路径,从`/api`改为`/api/auth` - 更新页面标题为"HLAE中文站" - 简化Providers组件,移除未使用的主题配置 - 添加Hero组件展示网站主标题和操作按钮 - 优化登录和注册页面的表单验证和UI - 更新全局样式,添加品牌颜色和主题变量 - 改进导航栏,添加图标和更好的响应式设计 - 优化资源管理模态框,添加图标字段支持
This commit is contained in:
@@ -6,7 +6,7 @@ export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: 'sqlite'
|
||||
}),
|
||||
basePath: '/api',
|
||||
basePath: '/api/auth',
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ const app = new Elysia()
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
})
|
||||
)
|
||||
.mount('/auth', auth.handler)
|
||||
.mount(auth.handler)
|
||||
.get('/health', () => 'OK')
|
||||
.use(resources)
|
||||
.use(posts)
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HLAE Site</title>
|
||||
<title>HLAE中文站</title>
|
||||
<script>
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.classList.add(theme);
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { Button } from "@heroui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Button, Card } from "@heroui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function AboutPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<h1>About</h1>
|
||||
<Button as={RouterLink} to="/">Back</Button>
|
||||
<div className="container mx-auto px-6 py-12 max-w-4xl">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<Card.Title className="text-3xl font-bold">关于 HLAE 中文站</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content className="prose dark:prose-invert">
|
||||
<p className="text-default-500 mb-6 leading-relaxed">
|
||||
HLAE 中文站是一个致力于为 CS 视频制作者提供工具和资源的社区。
|
||||
我们旨在简化创作流程,提供各种实用的工具、录像分析和下载加速服务。
|
||||
</p>
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<Button onPress={() => navigate("/")}>
|
||||
返回首页
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { Button } from "@heroui/react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { Button, Card } from "@heroui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function DemoPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 py-16 max-w-3xl mx-auto">
|
||||
<h1 className="text-2xl font-bold leading-loose">Demo Page</h1>
|
||||
<button onClick={() => navigate("/")} className="p-3 rounded-lg bg-zinc-300 transition hover:scale-105 cursor-pointer">Back</button>
|
||||
<Button color="primary" variant="flat" onPress={() => navigate("/")}>HeroUI Back</Button>
|
||||
<Link to="/">Back</Link>
|
||||
<div className="container mx-auto px-6 py-12 max-w-4xl">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<Card.Title className="text-3xl font-bold">击杀生成 (Kill Generation)</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p className="text-default-500 mb-4">
|
||||
这里是击杀生成工具页面。目前正在开发中,敬请期待!
|
||||
</p>
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<Button onPress={() => navigate("/")}>
|
||||
返回首页
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,139 @@
|
||||
import { useState } from 'react';
|
||||
import { SiteNavbar } from '../components/SiteNavbar';
|
||||
import { useResources, Resource } from '../hooks/useApi';
|
||||
import { useResources } from '../hooks/useApi';
|
||||
import { CustomCard } from '../components/CustomCard';
|
||||
import { Button, Link } from '@heroui/react';
|
||||
import { Hero } from '../components/Hero';
|
||||
import { Button, Link, Chip } from '@heroui/react';
|
||||
import { buttonVariants, linkVariants } from '@heroui/styles';
|
||||
import { CreateResourceModal } from '../components/CreateResourceModal';
|
||||
import { EditResourceModal } from '../components/EditResourceModal';
|
||||
import { useSession } from '../lib/auth-client';
|
||||
import {
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
BookOpenIcon,
|
||||
BookmarkIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
ListBulletIcon,
|
||||
VideoCameraIcon,
|
||||
UserGroupIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
const PORTALS = [
|
||||
{
|
||||
title: '中文论坛',
|
||||
description: 'HLAE中文交流社区',
|
||||
icon: ChatBubbleLeftEllipsisIcon,
|
||||
url: 'https://bbs.hlae.site',
|
||||
},
|
||||
{
|
||||
title: '官方Wiki',
|
||||
description: '权威,但是英文 orz',
|
||||
icon: BookOpenIcon,
|
||||
url: 'https://github.com/advancedfx/advancedfx/wiki',
|
||||
},
|
||||
{
|
||||
title: '新版文档',
|
||||
description: '新版 advancedfx 文档,建设中',
|
||||
icon: BookmarkIcon,
|
||||
url: 'https://docs.hlae.site',
|
||||
},
|
||||
{
|
||||
title: '官方Discord',
|
||||
description: '和开发者近距离交流',
|
||||
icon: UserGroupIcon,
|
||||
url: 'https://discord.gg/advancedfx',
|
||||
},
|
||||
{
|
||||
title: '问题与建议提交',
|
||||
description: 'tnnd 为什么不更新',
|
||||
icon: QuestionMarkCircleIcon,
|
||||
url: 'https://github.com/advancedfx/advancedfx/issues',
|
||||
},
|
||||
{
|
||||
title: 'HUD生成器',
|
||||
description: '击杀信息和准星生成工具',
|
||||
icon: WrenchScrewdriverIcon,
|
||||
url: '/hud-generator',
|
||||
},
|
||||
{
|
||||
title: '击杀信息生成',
|
||||
description: 'CS2 · CS 击杀信息生成工具(测试)',
|
||||
icon: ListBulletIcon,
|
||||
url: '/demo',
|
||||
},
|
||||
{
|
||||
title: 'HLTV',
|
||||
description: 'CSGO新闻、数据、录像',
|
||||
icon: VideoCameraIcon,
|
||||
url: 'https://www.hltv.org',
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const { resources, isLoading } = useResources();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [selectedResource, setSelectedResource] = useState<Resource | null>(null);
|
||||
const [selectedResource, setSelectedResource] = useState<any>(null);
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SiteNavbar />
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<section className="mb-12">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">传送门 & 资源</h2>
|
||||
<div className="pb-20">
|
||||
<main className="container mx-auto px-6">
|
||||
<Hero />
|
||||
{/* Portals Section */}
|
||||
<section className="mb-20">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">传送门</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{PORTALS.map((portal) => {
|
||||
const isExternal = portal.url.startsWith('http');
|
||||
const Content = (
|
||||
<CustomCard className="group cursor-pointer p-6 h-full">
|
||||
<div className="flex gap-4">
|
||||
<div className="p-3 rounded-2xl bg-default-100 text-default-600 group-hover:bg-primary-50 group-hover:text-primary transition-colors">
|
||||
<portal.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-lg font-bold">{portal.title}</h3>
|
||||
<p className="text-sm text-default-500 line-clamp-1">{portal.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CustomCard>
|
||||
);
|
||||
|
||||
return isExternal ? (
|
||||
<a
|
||||
key={portal.title}
|
||||
href={portal.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="no-underline text-inherit block"
|
||||
>
|
||||
{Content}
|
||||
</a>
|
||||
) : (
|
||||
<RouterLink
|
||||
key={portal.title}
|
||||
to={portal.url}
|
||||
className="no-underline text-inherit block"
|
||||
>
|
||||
{Content}
|
||||
</RouterLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Resources Section */}
|
||||
<section>
|
||||
<div className="flex justify-center items-center mb-12 relative">
|
||||
<h2 className="text-3xl font-bold">资源下载</h2>
|
||||
{session && (
|
||||
<Button onPress={() => setIsModalOpen(true)} color="primary" variant="secondary">
|
||||
<Button
|
||||
onPress={() => setIsModalOpen(true)}
|
||||
variant="tertiary"
|
||||
className="absolute right-0"
|
||||
>
|
||||
添加资源
|
||||
</Button>
|
||||
)}
|
||||
@@ -36,41 +147,73 @@ export default function HomePage() {
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div>加载中...</div>
|
||||
<div className="flex justify-center py-20">加载中...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{resources?.map((res) => (
|
||||
<CustomCard key={res.id} variant="glass" className="p-4">
|
||||
<CustomCard.Header>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-md">{res.title}</p>
|
||||
{res.category && <p className="text-small text-default-500">{res.category}</p>}
|
||||
<CustomCard key={res.id} className="p-6">
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-default-100 overflow-hidden flex-shrink-0 flex items-center justify-center">
|
||||
{res.icon ? (
|
||||
<img
|
||||
alt={res.title}
|
||||
src={res.icon}
|
||||
className="w-10 h-10 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-2xl font-bold text-default-400">
|
||||
{res.title[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CustomCard.Header>
|
||||
<CustomCard.Content>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">{res.description}</p>
|
||||
</CustomCard.Content>
|
||||
<CustomCard.Footer className="flex justify-between items-center gap-2">
|
||||
<Button as={Link} href={res.url} target="_blank" size="sm" color="primary">
|
||||
访问
|
||||
</Button>
|
||||
{session && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setSelectedResource(res);
|
||||
setIsEditModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</CustomCard.Footer>
|
||||
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold truncate">{res.title}</h3>
|
||||
{res.category && (
|
||||
<Chip size="sm" variant="soft" className="text-[10px] h-5">
|
||||
{res.category}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-default-500 line-clamp-2">{res.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-auto">
|
||||
<a
|
||||
href={res.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={buttonVariants({ fullWidth: true, className: "bg-[#C14B4B] text-white font-medium hover:opacity-90 flex items-center justify-center h-10 px-4 rounded-3xl" })}
|
||||
>
|
||||
加速下载
|
||||
</a>
|
||||
<a
|
||||
href={res.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={buttonVariants({ fullWidth: true, variant: "tertiary", className: "bg-default-200 text-default-600 font-medium hover:bg-default-300 flex items-center justify-center h-10 px-4 rounded-3xl" })}
|
||||
>
|
||||
原始下载
|
||||
</a>
|
||||
{session && (
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
setSelectedResource(res);
|
||||
setIsEditModalOpen(true);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<WrenchScrewdriverIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CustomCard>
|
||||
))}
|
||||
{!resources?.length && (
|
||||
<div className="col-span-full text-center text-gray-500">
|
||||
<div className="col-span-full text-center text-default-400 py-20">
|
||||
暂无资源
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Card, Input, Button, Form, toast } from '@heroui/react';
|
||||
import { Card, Input, Button, Form, toast, TextField, Label, FieldError } from '@heroui/react';
|
||||
import { signIn } from '../../lib/auth-client';
|
||||
|
||||
export default function LoginPage() {
|
||||
@@ -9,7 +9,7 @@ export default function LoginPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -32,32 +32,50 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<Card className="w-full max-w-md p-6">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">登录 HLAE 中文站</h1>
|
||||
<Form validationBehavior="native" onSubmit={handleLogin} className="flex flex-col gap-4">
|
||||
<Input
|
||||
isRequired
|
||||
label="邮箱"
|
||||
placeholder="请输入您的邮箱"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="密码"
|
||||
placeholder="请输入您的密码"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button color="primary" type="submit" isLoading={loading}>
|
||||
登录
|
||||
</Button>
|
||||
<div className="text-center text-sm">
|
||||
还没有账号? <Link to="/register" className="text-primary hover:underline">去注册</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)] p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<Card.Header>
|
||||
<Card.Title className="text-2xl font-bold text-center w-full">登录 HLAE 中文站</Card.Title>
|
||||
</Card.Header>
|
||||
<Form validationBehavior="native" onSubmit={handleLogin}>
|
||||
<Card.Content className="flex flex-col gap-6">
|
||||
<TextField
|
||||
isRequired
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
>
|
||||
<Label>邮箱</Label>
|
||||
<Input placeholder="请输入您的邮箱" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
isRequired
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
>
|
||||
<Label>密码</Label>
|
||||
<Input placeholder="请输入您的密码" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
</Card.Content>
|
||||
<Card.Footer className="flex flex-col gap-3 mt-2">
|
||||
<Button type="submit" isPending={loading} fullWidth className="font-bold">
|
||||
登录
|
||||
</Button>
|
||||
<Button as={Link} to="/register" variant="tertiary" fullWidth className="font-bold">
|
||||
注册账号
|
||||
</Button>
|
||||
<div className="flex justify-center mt-2">
|
||||
<Link to="/" className="text-sm text-default-500 hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span>← 返回首页</span>
|
||||
</Link>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useParams, Link as RouterLink } from 'react-router-dom';
|
||||
import { SiteNavbar } from '../../../components/SiteNavbar';
|
||||
import { usePost } from '../../../hooks/useApi';
|
||||
import { Card, Button, Avatar, Divider, Skeleton } from '@heroui/react';
|
||||
import { Link } from '@heroui/react';
|
||||
import { Card, Button, Avatar, Separator, Skeleton } from '@heroui/react';
|
||||
import { buttonVariants } from '@heroui/styles';
|
||||
|
||||
export default function PostDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -10,19 +9,12 @@ export default function PostDetailPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SiteNavbar />
|
||||
<div className="pb-20">
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<Card className="p-6 max-w-4xl mx-auto space-y-4">
|
||||
<Skeleton className="rounded-lg">
|
||||
<div className="h-8 w-3/4 rounded-lg bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="rounded-lg">
|
||||
<div className="h-4 w-1/4 rounded-lg bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="rounded-lg">
|
||||
<div className="h-40 w-full rounded-lg bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="h-8 w-3/4 rounded-lg" />
|
||||
<Skeleton className="h-4 w-1/4 rounded-lg" />
|
||||
<Skeleton className="h-40 w-full rounded-lg" />
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
@@ -31,44 +23,42 @@ export default function PostDetailPage() {
|
||||
|
||||
if (isError || !post) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SiteNavbar />
|
||||
<div className="pb-20">
|
||||
<main className="container mx-auto px-4 py-8 text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">帖子未找到</h1>
|
||||
<Button as={RouterLink} to="/posts" color="primary">
|
||||
<RouterLink to="/posts" className={buttonVariants({ variant: "secondary" })}>
|
||||
返回帖子列表
|
||||
</Button>
|
||||
</RouterLink>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SiteNavbar />
|
||||
<div className="pb-20">
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-4">
|
||||
<Link as={RouterLink} to="/posts" className="text-default-500 hover:text-foreground">
|
||||
<RouterLink to="/posts" className="text-default-500 hover:text-foreground">
|
||||
← 返回帖子列表
|
||||
</Link>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<Card className="p-6 mb-8">
|
||||
<Card.Header className="flex flex-col items-start gap-2">
|
||||
<h1 className="text-3xl font-bold">{post.title}</h1>
|
||||
<div className="flex items-center gap-2 text-small text-default-500">
|
||||
<Avatar
|
||||
className="w-6 h-6"
|
||||
name={post.user?.name || ''}
|
||||
src={undefined} // Add user image if available in API
|
||||
/>
|
||||
<Avatar className="w-6 h-6">
|
||||
<Avatar.Fallback>
|
||||
{(post.user?.name || 'U').slice(0, 2).toUpperCase()}
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<span>{post.user?.name || '匿名用户'}</span>
|
||||
<span>•</span>
|
||||
<span>{new Date(post.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Divider className="my-4" />
|
||||
<Separator className="my-4" />
|
||||
<Card.Content>
|
||||
<div className="whitespace-pre-wrap text-lg leading-relaxed">
|
||||
{post.content}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { SiteNavbar } from '../../components/SiteNavbar';
|
||||
import { usePosts } from '../../hooks/useApi';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Card, Button, Input, TextArea, Form, Link, toast } from '@heroui/react';
|
||||
import { Card, Button, Input, TextArea, Form, Link, toast, TextField, Label, FieldError } from '@heroui/react';
|
||||
import { useSession } from '../../lib/auth-client';
|
||||
import { api } from '../../api/client';
|
||||
|
||||
@@ -13,7 +12,7 @@ export default function PostsPage() {
|
||||
const [content, setContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!session) return;
|
||||
setIsSubmitting(true);
|
||||
@@ -36,8 +35,7 @@ export default function PostsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<SiteNavbar />
|
||||
<div className="pb-20">
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className="flex-1">
|
||||
@@ -50,9 +48,9 @@ export default function PostsPage() {
|
||||
<Card key={post.id} className="p-4">
|
||||
<Card.Header>
|
||||
<div className="flex flex-col">
|
||||
<Link as={RouterLink} to={`/posts/${post.id}`} className="text-md font-bold text-foreground hover:text-primary">
|
||||
<RouterLink to={`/posts/${post.id}`} className="text-md font-bold text-foreground hover:text-primary">
|
||||
{post.title}
|
||||
</Link>
|
||||
</RouterLink>
|
||||
<p className="text-small text-default-500">
|
||||
{post.user?.name || '匿名用户'} - {new Date(post.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
@@ -73,21 +71,29 @@ export default function PostsPage() {
|
||||
<Card className="p-4 sticky top-4">
|
||||
<h3 className="text-xl font-bold mb-4">发布新帖</h3>
|
||||
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
<TextField
|
||||
isRequired
|
||||
label="标题"
|
||||
placeholder="请输入标题"
|
||||
name="title"
|
||||
value={title}
|
||||
onValueChange={setTitle}
|
||||
/>
|
||||
<TextArea
|
||||
onChange={setTitle}
|
||||
>
|
||||
<Label>标题</Label>
|
||||
<Input placeholder="请输入标题" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
isRequired
|
||||
label="内容"
|
||||
placeholder="请输入内容"
|
||||
name="content"
|
||||
value={content}
|
||||
onValueChange={setContent}
|
||||
/>
|
||||
<Button color="primary" type="submit" isLoading={isSubmitting}>
|
||||
onChange={setContent}
|
||||
>
|
||||
<Label>内容</Label>
|
||||
<TextArea placeholder="请输入内容" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<Button type="submit" isPending={isSubmitting}>
|
||||
发布
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Card, Input, Button, Form, toast } from '@heroui/react';
|
||||
import { Card, Input, Button, Form, toast, TextField, Label, FieldError } from '@heroui/react';
|
||||
import { signUp } from '../../lib/auth-client';
|
||||
|
||||
export default function RegisterPage() {
|
||||
@@ -9,7 +9,7 @@ export default function RegisterPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
const handleRegister = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -33,32 +33,50 @@ export default function RegisterPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<Card className="w-full max-w-md p-6">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">注册 HLAE 中文站</h1>
|
||||
<Form validationBehavior="native" onSubmit={handleRegister} className="flex flex-col gap-4">
|
||||
<Input
|
||||
isRequired
|
||||
label="邮箱"
|
||||
placeholder="请输入您的邮箱"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="密码"
|
||||
placeholder="请输入您的密码"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Button color="primary" type="submit" isLoading={loading}>
|
||||
注册
|
||||
</Button>
|
||||
<div className="text-center text-sm">
|
||||
已有账号? <Link to="/login" className="text-primary hover:underline">去登录</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-64px)] p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<Card.Header>
|
||||
<Card.Title className="text-2xl font-bold text-center w-full">注册 HLAE 中文站</Card.Title>
|
||||
</Card.Header>
|
||||
<Form validationBehavior="native" onSubmit={handleRegister}>
|
||||
<Card.Content className="flex flex-col gap-6">
|
||||
<TextField
|
||||
isRequired
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
>
|
||||
<Label>邮箱</Label>
|
||||
<Input placeholder="请输入您的邮箱" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
isRequired
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={setPassword}
|
||||
>
|
||||
<Label>密码</Label>
|
||||
<Input placeholder="请输入您的密码" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
</Card.Content>
|
||||
<Card.Footer className="flex flex-col gap-3 mt-2">
|
||||
<Button type="submit" isPending={loading} fullWidth className="font-bold">
|
||||
注册
|
||||
</Button>
|
||||
<Button as={Link} to="/login" variant="tertiary" fullWidth className="font-bold">
|
||||
已有账号?登录
|
||||
</Button>
|
||||
<div className="flex justify-center mt-2">
|
||||
<Link to="/" className="text-sm text-default-500 hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span>← 返回首页</span>
|
||||
</Link>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Modal, Button, Input, Form, toast } from "@heroui/react";
|
||||
import { Modal, Button, Input, Form, toast, TextField, Label, FieldError } from "@heroui/react";
|
||||
import { api } from '../api/client';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
@@ -7,11 +7,12 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [icon, setIcon] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -19,6 +20,7 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
icon,
|
||||
category
|
||||
});
|
||||
mutate('/resources');
|
||||
@@ -26,6 +28,7 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setUrl('');
|
||||
setIcon('');
|
||||
setCategory('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -37,53 +40,80 @@ export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean,
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<Modal.Backdrop />
|
||||
<Modal.Container>
|
||||
<Modal.Dialog>
|
||||
<Modal.Backdrop>
|
||||
<Modal.Container>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>添加新资源</Modal.Heading>
|
||||
<Modal.CloseTrigger />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
<TextField
|
||||
isRequired
|
||||
label="标题"
|
||||
placeholder="资源名称"
|
||||
name="title"
|
||||
value={title}
|
||||
onValueChange={setTitle}
|
||||
/>
|
||||
<Input
|
||||
label="描述"
|
||||
placeholder="简短描述"
|
||||
onChange={setTitle}
|
||||
>
|
||||
<Label>标题</Label>
|
||||
<Input placeholder="资源名称" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
name="description"
|
||||
value={description}
|
||||
onValueChange={setDescription}
|
||||
/>
|
||||
<Input
|
||||
onChange={setDescription}
|
||||
>
|
||||
<Label>描述</Label>
|
||||
<Input placeholder="简短描述" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
isRequired
|
||||
label="链接"
|
||||
placeholder="https://..."
|
||||
name="url"
|
||||
value={url}
|
||||
onValueChange={setUrl}
|
||||
/>
|
||||
<Input
|
||||
label="分类"
|
||||
placeholder="如: 工具, 文档"
|
||||
onChange={setUrl}
|
||||
>
|
||||
<Label>链接</Label>
|
||||
<Input placeholder="https://..." variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
name="icon"
|
||||
value={icon}
|
||||
onChange={setIcon}
|
||||
>
|
||||
<Label>图标 URL</Label>
|
||||
<Input placeholder="如: /icon/hlae.png" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
name="category"
|
||||
value={category}
|
||||
onValueChange={setCategory}
|
||||
/>
|
||||
<Button color="primary" type="submit" isLoading={loading}>
|
||||
onChange={setCategory}
|
||||
>
|
||||
<Label>版本/分类</Label>
|
||||
<Input placeholder="如: v2.189.6" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<Button type="submit" isPending={loading}>
|
||||
提交
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button color="danger" variant="ghost" onPress={() => onOpenChange(false)}>
|
||||
<Button variant="tertiary" onPress={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Backdrop>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@ export const customStyles = tv({
|
||||
glass: "bg-white/70 backdrop-blur-md border-white/20 shadow-xl dark:bg-black/70 dark:border-white/10",
|
||||
neon: "border border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)] bg-gray-900/90 text-white",
|
||||
minimal: "border-none shadow-none bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
|
||||
modern: "bg-default-50/40 hover:bg-default-100/60 transition-all duration-500 border-none shadow-sm hover:shadow-xl hover:-translate-y-1 rounded-3xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "modern",
|
||||
}
|
||||
});
|
||||
|
||||
type CustomCardProps = React.ComponentProps<typeof Card> & VariantProps<typeof customStyles>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Button, Input, Form, toast } from "@heroui/react";
|
||||
import { Modal, Button, Input, Form, toast, TextField, Label, FieldError } from "@heroui/react";
|
||||
import { api } from '../api/client';
|
||||
import { useSWRConfig } from 'swr';
|
||||
import { Resource } from '../hooks/useApi';
|
||||
@@ -16,6 +16,7 @@ export function EditResourceModal({
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [icon, setIcon] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { mutate } = useSWRConfig();
|
||||
@@ -25,11 +26,12 @@ export function EditResourceModal({
|
||||
setTitle(resource.title);
|
||||
setDescription(resource.description || '');
|
||||
setUrl(resource.url);
|
||||
setIcon(resource.icon || '');
|
||||
setCategory(resource.category || '');
|
||||
}
|
||||
}, [resource]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!resource) return;
|
||||
setLoading(true);
|
||||
@@ -38,6 +40,7 @@ export function EditResourceModal({
|
||||
title,
|
||||
description,
|
||||
url,
|
||||
icon,
|
||||
category
|
||||
});
|
||||
mutate('/resources');
|
||||
@@ -67,58 +70,85 @@ export function EditResourceModal({
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<Modal.Backdrop />
|
||||
<Modal.Container>
|
||||
<Modal.Dialog>
|
||||
<Modal.Backdrop>
|
||||
<Modal.Container>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>编辑资源</Modal.Heading>
|
||||
<Modal.CloseTrigger />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Input
|
||||
<TextField
|
||||
isRequired
|
||||
label="标题"
|
||||
placeholder="资源名称"
|
||||
name="title"
|
||||
value={title}
|
||||
onValueChange={setTitle}
|
||||
/>
|
||||
<Input
|
||||
label="描述"
|
||||
placeholder="简短描述"
|
||||
onChange={setTitle}
|
||||
>
|
||||
<Label>标题</Label>
|
||||
<Input placeholder="资源名称" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
name="description"
|
||||
value={description}
|
||||
onValueChange={setDescription}
|
||||
/>
|
||||
<Input
|
||||
onChange={setDescription}
|
||||
>
|
||||
<Label>描述</Label>
|
||||
<Input placeholder="简短描述" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
isRequired
|
||||
label="链接"
|
||||
placeholder="https://..."
|
||||
name="url"
|
||||
value={url}
|
||||
onValueChange={setUrl}
|
||||
/>
|
||||
<Input
|
||||
label="分类"
|
||||
placeholder="如: 工具, 文档"
|
||||
onChange={setUrl}
|
||||
>
|
||||
<Label>链接</Label>
|
||||
<Input placeholder="https://..." variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
name="icon"
|
||||
value={icon}
|
||||
onChange={setIcon}
|
||||
>
|
||||
<Label>图标 URL</Label>
|
||||
<Input placeholder="如: /icon/hlae.png" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
name="category"
|
||||
value={category}
|
||||
onValueChange={setCategory}
|
||||
/>
|
||||
onChange={setCategory}
|
||||
>
|
||||
<Label>版本/分类</Label>
|
||||
<Input placeholder="如: v2.189.6" variant="secondary" />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
|
||||
<div className="flex justify-between gap-2 mt-4">
|
||||
<Button color="danger" variant="flat" onPress={handleDelete} isLoading={loading}>
|
||||
<Button variant="danger" onPress={handleDelete} isPending={loading}>
|
||||
删除资源
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button color="danger" variant="ghost" onPress={() => onOpenChange(false)}>
|
||||
<Button variant="ghost" onPress={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="primary" type="submit" isLoading={loading}>
|
||||
<Button type="submit" isPending={loading}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Backdrop>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
51
web/src/components/Hero.tsx
Normal file
51
web/src/components/Hero.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Button } from '@heroui/react';
|
||||
import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export const Hero = () => {
|
||||
return (
|
||||
<section className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<h1 className="text-6xl font-extrabold tracking-tight mb-8">
|
||||
HLAE中文站
|
||||
</h1>
|
||||
<p className="text-2xl text-default-600 mb-12 max-w-3xl leading-relaxed">
|
||||
<span className="text-[#FF6B00] font-bold">CS</span>等起源引擎游戏的影片制作工具
|
||||
<span className="text-brand font-bold mx-1">HLAE</span>的中文门户网站
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<Button
|
||||
as="a"
|
||||
href="https://www.advancedfx.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-brand text-white font-medium px-10 h-14 rounded-full text-lg shadow-lg shadow-brand/20 hover:opacity-90 transition-opacity"
|
||||
startContent={<WrenchScrewdriverIcon className="w-6 h-6" />}
|
||||
>
|
||||
官方网站
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="https://github.com/purp1e/hlae-site"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-default-100 text-default-900 font-medium px-10 h-14 rounded-full text-lg hover:bg-default-200 transition-colors"
|
||||
startContent={
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
本项目
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -5,8 +5,11 @@ import {
|
||||
Dropdown,
|
||||
Label
|
||||
} from "@heroui/react";
|
||||
import { linkVariants, buttonVariants } from "@heroui/styles";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession, signOut } from "../lib/auth-client";
|
||||
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
export function SiteNavbar() {
|
||||
const { data: session } = useSession();
|
||||
@@ -27,66 +30,74 @@ export function SiteNavbar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-between px-6 py-3 bg-background/70 backdrop-blur-md border-b border-white/10 sticky top-0 z-50 w-full">
|
||||
<nav className="flex items-center justify-between px-8 py-4 bg-background/70 backdrop-blur-md border-b border-default-100 sticky top-0 z-50 w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link className="font-bold text-inherit text-lg no-underline text-foreground" href="/">
|
||||
HLAE 中文站
|
||||
</Link>
|
||||
<RouterLink to="/" className="font-bold text-inherit text-xl no-underline text-foreground">
|
||||
HLAE中文站
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex gap-6 justify-center flex-1">
|
||||
<Link className="text-foreground hover:text-primary transition-colors" href="/">
|
||||
首页
|
||||
</Link>
|
||||
<Link className="text-foreground hover:text-primary transition-colors" href="/posts">
|
||||
帖子
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="hidden sm:flex gap-8 items-center">
|
||||
<RouterLink to="/" className="text-foreground/80 hover:text-foreground font-medium transition-colors">
|
||||
主页
|
||||
</RouterLink>
|
||||
<RouterLink to="/demo" className="text-foreground/80 hover:text-foreground font-medium transition-colors">
|
||||
击杀生成
|
||||
</RouterLink>
|
||||
<RouterLink to="/about" className="text-foreground/80 hover:text-foreground font-medium transition-colors">
|
||||
关于
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 justify-end">
|
||||
<Button variant="ghost" onPress={toggleTheme}>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</Button>
|
||||
{session ? (
|
||||
<Dropdown>
|
||||
<Dropdown.Trigger>
|
||||
<button className="outline-none focus:outline-none rounded-full">
|
||||
<Avatar size="sm">
|
||||
<Avatar.Image
|
||||
alt={session.user.name || "用户头像"}
|
||||
src={session.user.image || ""}
|
||||
/>
|
||||
<Avatar.Fallback>
|
||||
{(session.user.name || "U").slice(0, 2).toUpperCase()}
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Popover>
|
||||
<Dropdown.Menu aria-label="Profile Actions">
|
||||
<Dropdown.Item id="profile" textValue="Profile">
|
||||
<Label className="font-semibold">登录为 {session.user.email}</Label>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id="logout" textValue="Logout" onPress={() => signOut()}>
|
||||
<Label className="text-danger">退出登录</Label>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Popover>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className="flex gap-4 items-center">
|
||||
<Link href="/login">
|
||||
<Button variant="ghost" className="text-foreground hover:text-primary">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={toggleTheme}
|
||||
className="text-foreground/80"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<SunIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<MoonIcon className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{session ? (
|
||||
<Dropdown>
|
||||
<Dropdown.Trigger>
|
||||
<Button isIconOnly variant="tertiary" className="rounded-full">
|
||||
<Avatar size="sm" color="accent">
|
||||
<Avatar.Image
|
||||
alt={session.user.name || "用户头像"}
|
||||
src={session.user.image || ""}
|
||||
/>
|
||||
<Avatar.Fallback>
|
||||
{(session.user.name || "U").slice(0, 2).toUpperCase()}
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Popover>
|
||||
<Dropdown.Menu aria-label="Profile Actions">
|
||||
<Dropdown.Item id="profile" textValue="Profile">
|
||||
<Label className="font-semibold">登录为 {session.user.email}</Label>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id="logout" textValue="Logout" onPress={() => signOut()}>
|
||||
<Label className="text-danger">退出登录</Label>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Popover>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className="flex gap-4 items-center">
|
||||
<RouterLink to="/login" className={buttonVariants({ variant: "light", className: "text-foreground font-medium" })}>
|
||||
登录
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/register">
|
||||
<Button variant="primary" className="text-foreground hover:text-primary">
|
||||
注册
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</RouterLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
@import "tailwindcss";
|
||||
@import "@heroui/styles";
|
||||
|
||||
@layer base {
|
||||
/* :root {} */
|
||||
@theme {
|
||||
--color-brand: #C14B4B;
|
||||
--color-brand-foreground: #FFFFFF;
|
||||
|
||||
--radius-2xl: 1.25rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased selection:bg-brand/20 selection:text-brand;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Toast } from "@heroui/react";
|
||||
import "./globals.css";
|
||||
// import Footer from "./components/footer";
|
||||
// import Header from "./components/header";
|
||||
import { SiteNavbar } from "./components/SiteNavbar";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -9,11 +8,10 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<Toast.Provider />
|
||||
{/* <Header /> */}
|
||||
<SiteNavbar />
|
||||
<main className="min-h-screen h-full">{children}</main>
|
||||
{/* <Footer /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
// import { ThemeProvider } from "next-themes";
|
||||
// import { HeroUIProvider, ToastProvider } from '@heroui/react'
|
||||
|
||||
export function Providers( { children }: { children: React.ReactNode } ) {
|
||||
return (
|
||||
// <HeroUIProvider>
|
||||
// <ThemeProvider attribute="class" defaultTheme="dark">
|
||||
// <ToastProvider />
|
||||
<>
|
||||
{children}
|
||||
// </ThemeProvider>
|
||||
// </HeroUIProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StrictMode, Suspense, useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter, useRoutes } from "react-router-dom";
|
||||
// import { Providers } from "./providers";
|
||||
import { Providers } from "./providers";
|
||||
import RootLayout from "./layout";
|
||||
import routes from "~react-pages";
|
||||
|
||||
@@ -28,11 +28,11 @@ const app = createRoot(document.getElementById("root")!);
|
||||
app.render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
{/* <Providers> */}
|
||||
<RootLayout>
|
||||
<App />
|
||||
</RootLayout>
|
||||
{/* </Providers> */}
|
||||
<Providers>
|
||||
<RootLayout>
|
||||
<App />
|
||||
</RootLayout>
|
||||
</Providers>
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user