feat: 初始化 Toolso.AI 开源 AI 工具导航站项目

🎯 项目概述
- 一个开源的 AI 工具导航站,帮助用户发现和探索优秀的 AI 工具
- 基于 Next.js 16 + React 19 构建的现代化全栈应用

✨ 核心功能
- AI 工具目录:分类展示、搜索筛选、工具详情页
- 工具提交:用户可提交新工具,社区驱动
- 管理后台:工具审核、用户管理、角色权限控制
- 博客系统:MDX 驱动,AI 资讯和教程
- 国际化支持:中文/英文双语 (next-intl)
- 用户认证:Better Auth + Google OAuth
- 深色/浅色主题切换

🛠 技术栈
- 前端:Next.js 16 (App Router) + React 19 + TypeScript
- 样式:Tailwind CSS + Framer Motion 动画
- UI 组件:自定义组件库 + Radix UI
- 表单验证:React Hook Form + Zod
- 数据库:PostgreSQL + Drizzle ORM
- 认证:Better Auth (邮箱密码 + Google OAuth)
- 邮件:Resend (验证邮件 + Newsletter)
- 存储:Cloudflare R2 / AWS S3 兼容

📁 项目结构
- src/app/[locale]/(auth) - 认证页面
- src/app/[locale]/(marketing) - 营销页面 (首页、博客、工具目录)
- src/app/[locale]/(protected) - 受保护页面 (仪表板、个人资料)
- src/app/[locale]/(admin) - 管理后台
- src/features - 功能模块
- src/lib - 核心业务逻辑
- messages - 国际化翻译文件

🗄 数据库
- 包含 user, session, account, verification, passwordResetToken 等核心表
- 支持 Drizzle ORM 迁移管理

📝 文档
- 完整的 CLAUDE.md 项目指南
- 环境变量配置示例 (.env.example)
- MIT 开源许可证
This commit is contained in:
CoolChange.AI
2026-01-03 23:52:28 +08:00
commit 8976caaaad
191 changed files with 37184 additions and 0 deletions
@@ -0,0 +1,188 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/button";
import { LocaleLink } from "@/components/locale-link";
import { FormShell } from "@/features/forms/components/form-shell";
import { AuthMessageCard } from "@/features/forms/components/auth-message-card";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/features/forms/components/input";
import { toast } from "sonner";
const resetPasswordSchema = z.object({
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
export default function ResetPasswordPage() {
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations();
const [isLoading, setIsLoading] = useState(false);
const [isValidToken, setIsValidToken] = useState<boolean | null>(null);
const token = searchParams.get("token");
const form = useForm<ResetPasswordInput>({
resolver: zodResolver(resetPasswordSchema),
defaultValues: {
password: "",
confirmPassword: "",
},
});
useEffect(() => {
if (!token) {
setIsValidToken(false);
return;
}
// Verify token validity
fetch(`/api/auth/verify-reset-token?token=${token}`)
.then(res => res.json())
.then(data => {
setIsValidToken(data.valid);
})
.catch(() => {
setIsValidToken(false);
});
}, [token]);
const onSubmit = async (data: ResetPasswordInput) => {
if (!token) {
toast.error("Invalid reset link");
return;
}
setIsLoading(true);
try {
const response = await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token,
password: data.password,
}),
});
if (response.ok) {
toast.success("Password reset successfully!");
setTimeout(() => {
router.push("/login");
}, 2000);
} else {
const error = await response.json();
toast.error(error.message || "Failed to reset password");
}
} catch (error) {
toast.error("An error occurred. Please try again.");
} finally {
setIsLoading(false);
}
};
if (isValidToken === null) {
return (
<AuthMessageCard
title="Verifying..."
description="Please wait while we verify your reset link."
>
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-foreground"></div>
</div>
</AuthMessageCard>
);
}
if (isValidToken === false) {
return (
<AuthMessageCard
title="Invalid or Expired Link"
description="This password reset link is invalid or has expired."
>
<p className="text-sm text-muted-foreground">
Please request a new password reset link.
</p>
<LocaleLink href="/forgot-password">
<Button className="w-full">
Request New Link
</Button>
</LocaleLink>
<LocaleLink href="/login">
<Button variant="outline" className="w-full">
Back to Login
</Button>
</LocaleLink>
</AuthMessageCard>
);
}
return (
<FormShell<ResetPasswordInput>
form={form}
title="Reset your password"
description="Enter your new password below."
onSubmit={onSubmit}
submitText="Reset Password"
submitLoadingText="Resetting..."
isLoading={isLoading}
footer={
<div className="text-center">
<LocaleLink
href="/login"
className="text-sm text-muted-foreground hover:text-foreground"
>
Back to Login
</LocaleLink>
</div>
}
>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter new password"
disabled={isLoading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Confirm new password"
disabled={isLoading}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormShell>
);
}