From 14af0aaa3ae1ec0f0ab5e504af6d8410c3c81443 Mon Sep 17 00:00:00 2001 From: AI Directory Platform Date: Wed, 9 Sep 2026 06:02:16 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=9B=E9=A1=B9=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=88=E5=AD=97=E5=8F=B7/=E5=93=81?= =?UTF-8?q?=E7=89=8C=E7=99=BB=E5=BD=95=E6=A8=A1=E6=9D=BF/=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E6=96=B9=E7=99=BB=E5=BD=95/=E5=85=8D=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E9=AA=8C=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 全局字号提升一档:body 基准 16.8px,14 个核心组件 99 处 text-xs/text-sm 放大 ② 五站五套登录模板:FormShell 品牌分栏布局(左品牌色侧栏+右表单), 每站独立 accent(近黑/橙红/纸墨棕/终端绿/紫),站点字标替换 Toolso.AI Logo; AuthLayout 简化为容器,布局职责下沉 FormShell ③ 第三方登录按钮:未配置 OAuth 凭据时显示 Google/GitHub 按钮, 走 /api/auth/mock-social 一键创建演示 session(生产配置凭据后自动 403 切真实流程); 按钮重设计为全宽方形(替换胶囊 Button) ④ 开发阶段免邮箱验证:better-auth databaseHooks 注册即 emailVerified=true, sendOnSignUp 仅生产开启;注册成功直接回跳原页面(跳过 check-email) 回归验证:字号 16.8px / 五站品牌色各异 / mock 登录闭环(投票→登录→回原页)/ 注册免验证直接进站(emailVerified=t + 跳首页)全部 PASS --- packages/core/messages/en.json | 5 +- packages/core/messages/zh.json | 5 +- packages/core/scripts/generate-site.mjs | 11 ++ packages/core/src/components/tool-card.tsx | 8 +- .../features/auth/components/login-form.tsx | 3 + .../features/auth/components/signup-form.tsx | 41 +++++- .../auth/components/social-auth-buttons.tsx | 109 +++++++++----- .../claim/components/claim-button.tsx | 8 +- .../claim/components/claims-table.tsx | 14 +- .../components/discovery-manager.tsx | 22 +-- .../features/forms/components/form-shell.tsx | 139 ++++++++++++------ .../reviews/components/reviews-section.tsx | 36 ++--- .../components/submissions-table.tsx | 20 +-- .../components/submit-tool-form.tsx | 30 ++-- .../tools/components/favorite-button.tsx | 2 +- .../features/tools/components/tools-list.tsx | 26 ++-- .../voting/components/vote-button.tsx | 8 +- packages/core/src/kit/theme-footer.tsx | 10 +- packages/core/src/kit/theme-header.tsx | 10 +- packages/core/src/kit/types.ts | 4 + packages/core/src/layouts/auth-layout.tsx | 43 +----- packages/core/src/lib/auth.ts | 27 ++++ packages/core/src/pages/[locale]/layout.tsx | 4 +- .../src/pages/api/auth/mock-social/route.ts | 64 ++++++++ sites/ai-stack/.env.example | 6 + sites/ai-stack/messages/en.json | 8 +- sites/ai-stack/messages/zh.json | 8 +- .../src/app/[locale]/(marketing)/loading.tsx | 2 + .../src/app/api/auth/mock-social/route.ts | 2 + sites/ai-stack/src/site/brand.tsx | 1 + sites/devai-tools/.env.example | 6 + sites/devai-tools/messages/en.json | 8 +- sites/devai-tools/messages/zh.json | 8 +- .../src/app/[locale]/(marketing)/loading.tsx | 2 + .../src/app/api/auth/mock-social/route.ts | 2 + sites/devai-tools/src/site/brand.tsx | 1 + sites/findai/.env.example | 6 + sites/findai/messages/en.json | 8 +- sites/findai/messages/zh.json | 8 +- .../src/app/[locale]/(marketing)/loading.tsx | 2 + .../src/app/api/auth/mock-social/route.ts | 2 + sites/findai/src/site/brand.tsx | 1 + sites/launchai/.env.example | 6 + sites/launchai/messages/en.json | 8 +- sites/launchai/messages/zh.json | 8 +- .../src/app/[locale]/(marketing)/loading.tsx | 2 + .../src/app/api/auth/mock-social/route.ts | 2 + sites/launchai/src/site/brand.tsx | 1 + sites/the-ai-ledger/.env.example | 6 + sites/the-ai-ledger/messages/en.json | 8 +- sites/the-ai-ledger/messages/zh.json | 8 +- .../src/app/[locale]/(marketing)/loading.tsx | 2 + .../src/app/api/auth/mock-social/route.ts | 2 + sites/the-ai-ledger/src/site/brand.tsx | 1 + 54 files changed, 537 insertions(+), 247 deletions(-) create mode 100644 packages/core/src/pages/api/auth/mock-social/route.ts create mode 100644 sites/ai-stack/src/app/[locale]/(marketing)/loading.tsx create mode 100644 sites/ai-stack/src/app/api/auth/mock-social/route.ts create mode 100644 sites/devai-tools/src/app/[locale]/(marketing)/loading.tsx create mode 100644 sites/devai-tools/src/app/api/auth/mock-social/route.ts create mode 100644 sites/findai/src/app/[locale]/(marketing)/loading.tsx create mode 100644 sites/findai/src/app/api/auth/mock-social/route.ts create mode 100644 sites/launchai/src/app/[locale]/(marketing)/loading.tsx create mode 100644 sites/launchai/src/app/api/auth/mock-social/route.ts create mode 100644 sites/the-ai-ledger/src/app/[locale]/(marketing)/loading.tsx create mode 100644 sites/the-ai-ledger/src/app/api/auth/mock-social/route.ts diff --git a/packages/core/messages/en.json b/packages/core/messages/en.json index f4509ec..8758fff 100644 --- a/packages/core/messages/en.json +++ b/packages/core/messages/en.json @@ -209,7 +209,10 @@ "social": { "googleSignIn": "Continue with Google", "or": "Or continue with", - "githubSignIn": "Continue with GitHub" + "githubSignIn": "Continue with GitHub", + "orContinueWith": "or continue with", + "loading": "Signing in...", + "mockFailed": "Social sign-in is unavailable right now. Use email instead." }, "checkEmail": { "title": "Check Your Email", diff --git a/packages/core/messages/zh.json b/packages/core/messages/zh.json index 13ba2db..b0a562f 100644 --- a/packages/core/messages/zh.json +++ b/packages/core/messages/zh.json @@ -209,7 +209,10 @@ "social": { "googleSignIn": "使用 Google 继续", "or": "或者继续使用", - "githubSignIn": "使用 GitHub 登录" + "githubSignIn": "使用 GitHub 登录", + "orContinueWith": "或使用以下方式", + "loading": "登录中...", + "mockFailed": "第三方登录暂不可用,请使用邮箱登录。" }, "checkEmail": { "title": "请查收您的邮件", diff --git a/packages/core/scripts/generate-site.mjs b/packages/core/scripts/generate-site.mjs index 2ea6c9f..7cfdf6c 100644 --- a/packages/core/scripts/generate-site.mjs +++ b/packages/core/scripts/generate-site.mjs @@ -23,6 +23,7 @@ const SITES_ROOT = path.join(REPO_ROOT, "sites"); // ==================================================================== const SITES = { "ai-stack": { + accent: "222 47% 11%", // 近黑(专业) port: 3001, theme: "classic", brand: { @@ -81,6 +82,7 @@ const SITES = { }, }, launchai: { + accent: "16 85% 55%", // 橙红(产品社区) port: 3002, theme: "product", brand: { @@ -137,6 +139,7 @@ const SITES = { }, }, "the-ai-ledger": { + accent: "36 70% 40%", // 纸墨棕(编辑部) port: 3003, theme: "editorial", brand: { @@ -194,6 +197,7 @@ const SITES = { }, }, "devai-tools": { + accent: "150 60% 30%", // 终端绿(开发者) port: 3004, theme: "niche", brand: { @@ -249,6 +253,7 @@ const SITES = { }, }, findai: { + accent: "260 70% 52%", // 紫(极简搜索) port: 3005, theme: "minimal", brand: { @@ -684,6 +689,12 @@ RESEND_API_KEY="" RESEND_FROM_EMAIL="${site.brand.name} " NEXT_PUBLIC_APP_URL="http://localhost:${site.port}" NEXT_PUBLIC_APP_NAME="${site.brand.name}" +# 登录页品牌模板(分栏侧栏颜色与标语) +NEXT_PUBLIC_AUTH_ACCENT="${site.accent}" +NEXT_PUBLIC_AUTH_BRAND_TAGLINE="${site.brand.tagline}" +# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件) +DEV_SKIP_EMAIL_VERIFY="true" +NEXT_PUBLIC_DEV_SKIP_EMAIL_VERIFY="true" ` ); diff --git a/packages/core/src/components/tool-card.tsx b/packages/core/src/components/tool-card.tsx index 99403ea..dcda0bf 100644 --- a/packages/core/src/components/tool-card.tsx +++ b/packages/core/src/components/tool-card.tsx @@ -70,7 +70,7 @@ export function ToolCard({ )} > {typeof rank === "number" && ( - + {rank} )} @@ -85,7 +85,7 @@ export function ToolCard({
-

+

{name}

{pricing && ( @@ -94,7 +94,7 @@ export function ToolCard({ )} {(tool.ratingAvg ?? 0) > 0 && ( - + {(tool.ratingAvg ?? 0).toFixed(1)} @@ -103,7 +103,7 @@ export function ToolCard({ {tagline && (

diff --git a/packages/core/src/features/auth/components/login-form.tsx b/packages/core/src/features/auth/components/login-form.tsx index 65e7ca9..a0d938b 100644 --- a/packages/core/src/features/auth/components/login-form.tsx +++ b/packages/core/src/features/auth/components/login-form.tsx @@ -109,6 +109,9 @@ export function LoginForm() { isLoading={isLoading} /> } + brandName={process.env.NEXT_PUBLIC_APP_NAME} + brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE} + accent={process.env.NEXT_PUBLIC_AUTH_ACCENT} >

} - socialSlot={} + socialSlot={ + + } + brandName={process.env.NEXT_PUBLIC_APP_NAME} + brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE} + accent={process.env.NEXT_PUBLIC_AUTH_ACCENT} > Promise | void; + onGithubSignIn?: (callbackURL?: string) => Promise | void; + isLoading?: boolean; +} /** 点击时读取 ?redirect= 参数(避免 useSearchParams 的 Suspense 要求) */ function getCallbackURL(): string { @@ -10,57 +17,81 @@ function getCallbackURL(): string { return new URLSearchParams(window.location.search).get("redirect") || "/"; } -interface SocialAuthButtonsProps { - onGoogleSignIn: (callbackURL?: string) => Promise | void; - onGithubSignIn?: (callbackURL?: string) => Promise | void; - isLoading?: boolean; -} - /** * # 组件 SocialAuthButtons * 说明:第三方登录按钮组(Google / GitHub)。 - * 回跳地址取 ?redirect= 参数(投票/评论未登录时跳登录后自动返回原页面)。 + * - 配置了真实 OAuth 凭据(NEXT_PUBLIC_AUTH_GOOGLE_ID / NEXT_PUBLIC_AUTH_GITHUB_ID)→ 走 better-auth OAuth 跳转。 + * - 开发/演示环境未配置凭据 → 按钮依然显示,点击调用 /api/auth/mock-social 一键创建演示 session + * (生产配置凭据后 mock 端点自动 403,按钮自动走真实流程)。 + * 回跳地址取 ?redirect= 参数(投票/评论未登录时跳登录后自动返回原页面)。 */ export function SocialAuthButtons({ onGoogleSignIn, onGithubSignIn, isLoading, }: SocialAuthButtonsProps) { - const t = useTranslations('auth.social'); - const googleEnabled = process.env.NEXT_PUBLIC_AUTH_GOOGLE_ENABLED !== "false"; - const githubEnabled = process.env.NEXT_PUBLIC_AUTH_GITHUB_ENABLED === "true"; + const t = useTranslations("auth.social"); + const router = useRouter(); + const locale = useLocale(); + const [mockLoading, setMockLoading] = useState(null); + const [mockError, setMockError] = useState(false); - const showGoogle = googleEnabled && !!onGoogleSignIn; - const showGithub = githubEnabled && !!onGithubSignIn; + const realGoogle = !!process.env.NEXT_PUBLIC_AUTH_GOOGLE_ID; + const realGithub = !!process.env.NEXT_PUBLIC_AUTH_GITHUB_ID; - if (!showGoogle && !showGithub) return null; + const mockSignIn = async (provider: "google" | "github") => { + setMockError(false); + setMockLoading(provider); + try { + const res = await fetch("/api/auth/mock-social", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider }), + }); + if (!res.ok) throw new Error("mock failed"); + // session 已建立,回跳原页面 + const target = getCallbackURL(); + window.location.assign(`/${locale}${target === "/" ? "" : target}`); + } catch { + setMockLoading(null); + setMockError(true); + } + }; + + const loading = isLoading || mockLoading !== null; + + const btnClass = + "flex w-full items-center justify-center gap-2.5 rounded-lg border border-border bg-background px-4 py-2.5 text-[0.95rem] font-medium text-foreground transition-colors hover:bg-secondary/60 disabled:opacity-50"; return ( -
- {showGoogle && ( - - )} - {showGithub && ( - +
+ + + {mockError && ( +

{t("mockFailed")}

)}
); } + +export { getCallbackURL }; diff --git a/packages/core/src/features/claim/components/claim-button.tsx b/packages/core/src/features/claim/components/claim-button.tsx index 7029184..28969e8 100644 --- a/packages/core/src/features/claim/components/claim-button.tsx +++ b/packages/core/src/features/claim/components/claim-button.tsx @@ -31,7 +31,7 @@ export function ClaimToolButton({ if (claimed) { return ( - + {t("verifiedOwner")} @@ -68,7 +68,7 @@ export function ClaimToolButton({ + + {footer} +
+
+ ); + + if (accent) { + return ( +
+ {/* 品牌分栏模板:col-span 全宽(覆盖 AuthLayout 的 2 列网格),左品牌右表单 */} +
+
+
+ + {brand.slice(0, 1)} + + {brand} +
+
+

+ {brandTagline || `Welcome back to ${brand}.`} +

+

+ Vote, review and save the AI tools you love — join the community. +

+
+

© {new Date().getFullYear()} {brand}

+
+
+ {formColumn} +
+
+
+ ); + } return (
@@ -51,53 +140,7 @@ export function FormShell({ className )} > -
-
-
{headerContent}
-

- {title} -

- {description ? ( -

- {description} -

- ) : null} -
- -
- - {children} - - {error ? ( -
- {error} -
- ) : null} - - - - {footer} - -
- - {socialSlot ? ( -
-
- -
{socialSlot}
-
- ) : null} -
+ {formColumn}
); diff --git a/packages/core/src/features/reviews/components/reviews-section.tsx b/packages/core/src/features/reviews/components/reviews-section.tsx index 9be9faa..f3ffd82 100644 --- a/packages/core/src/features/reviews/components/reviews-section.tsx +++ b/packages/core/src/features/reviews/components/reviews-section.tsx @@ -144,7 +144,7 @@ export function ReviewsSection({ )} {reviews.length === 0 && !formOpen ? ( -

+

{t("empty")}

) : ( @@ -156,11 +156,11 @@ export function ReviewsSection({ >
-
+
{review.authorName.slice(0, 1).toUpperCase()}
-
+
{review.authorName} {review.authorEmailVerified && ( @@ -168,7 +168,7 @@ export function ReviewsSection({
- + {new Date(review.createdAt).toLocaleDateString( locale === "zh" ? "zh-CN" : "en-US" )} @@ -181,8 +181,8 @@ export function ReviewsSection({ onClick={() => onHelpful(review.id)} className={ helpfulIds.has(review.id) - ? "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2.5 py-1 text-xs text-primary" - : "inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" + ? "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2.5 py-1 text-[0.85rem] text-primary" + : "inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[0.85rem] text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" } > @@ -191,24 +191,24 @@ export function ReviewsSection({
{review.title && ( -

{review.title}

+

{review.title}

)} -

+

{review.content}

{(review.pros || review.cons) && (
{review.pros && ( -
- +
+ {t("pros")} {review.pros}
)} {review.cons && ( -
- +
+ {t("cons")} {review.cons} @@ -286,7 +286,7 @@ function ReviewForm({ }; const inputClass = - "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"; + "w-full rounded-lg border border-border bg-background px-3 py-2 text-[0.95rem] text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"; return (
- + {t("yourRating")} @@ -337,7 +337,7 @@ function ReviewForm({ maxLength={200} onChange={(e) => setUseCase(e.target.value)} /> - {error &&

{error}

} + {error &&

{error}

}
diff --git a/packages/core/src/features/submission/components/submit-tool-form.tsx b/packages/core/src/features/submission/components/submit-tool-form.tsx index d592f52..9af9b5c 100644 --- a/packages/core/src/features/submission/components/submit-tool-form.tsx +++ b/packages/core/src/features/submission/components/submit-tool-form.tsx @@ -92,12 +92,12 @@ export function SubmitToolForm({ categories }: { categories: CategoryOption[] }) }; const inputClass = - "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"; + "w-full rounded-lg border border-border bg-background px-3 py-2 text-[0.95rem] text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"; return ( {duplicates.length > 0 && ( -
+
{t("duplicateFound")} @@ -107,7 +107,7 @@ export function SubmitToolForm({ categories }: { categories: CategoryOption[] })