refactor: simplify internationalization messages and remove unused content
- Update English and Chinese message files to remove unnecessary translations - Remove root page redirect component - Update home page to use new 'HomePage' translation key - Modify marketing links by commenting out docs link - Adjust locale selector width for better UI consistency
This commit is contained in:
parent
6646471e00
commit
508d35cc21
@ -1,5 +1,61 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "next-intl example"
|
||||
},
|
||||
"NotFoundPage": {
|
||||
"title": "404",
|
||||
"message": "Sorry, the page you are looking for does not exist.",
|
||||
"backToHome": "Back to home"
|
||||
},
|
||||
"ErrorPage": {
|
||||
"title": "Oops! Something went wrong!",
|
||||
"tryAgain": "Try again",
|
||||
"backToHome": "Back to home"
|
||||
},
|
||||
"AuthPage": {
|
||||
"login": {
|
||||
"title": "Login",
|
||||
"welcomeBack": "Welcome back",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"signIn": "Sign In",
|
||||
"signUp": "Don't have an account? Sign up",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"signInWithGoogle": "Sign In with Google",
|
||||
"signInWithGitHub": "Sign In with GitHub",
|
||||
"termsOfService": "Terms of Service",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"byClickingContinue": "By clicking continue, you agree to our",
|
||||
"and": "and"
|
||||
},
|
||||
"register": {
|
||||
"title": "Register",
|
||||
"createAccount": "Create an account",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"signIn": "Sign In",
|
||||
"signUp": "Already have an account? Sign in",
|
||||
"checkEmail": "Please check your email inbox"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Forgot Password",
|
||||
"email": "Email",
|
||||
"send": "Send",
|
||||
"backToLogin": "Back to login",
|
||||
"checkEmail": "Please check your email inbox"
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "Reset Password",
|
||||
"password": "Password",
|
||||
"reset": "Reset",
|
||||
"backToLogin": "Back to login"
|
||||
},
|
||||
"error": {
|
||||
"title": "Oops! Something went wrong!",
|
||||
"tryAgain": "Please try again.",
|
||||
"backToLogin": "Back to login",
|
||||
"checkEmail": "Please check your email inbox"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,61 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "next-intl 示例"
|
||||
}
|
||||
},
|
||||
"NotFoundPage": {
|
||||
"title": "404",
|
||||
"message": "抱歉,您正在寻找的页面不存在。",
|
||||
"backToHome": "返回首页"
|
||||
},
|
||||
"ErrorPage": {
|
||||
"title": "哎呀!出错了!",
|
||||
"tryAgain": "重试",
|
||||
"backToHome": "返回首页"
|
||||
},
|
||||
"AuthPage": {
|
||||
"login": {
|
||||
"title": "登录",
|
||||
"welcomeBack": "欢迎回来",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"signIn": "登录",
|
||||
"signUp": "没有账号?注册",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"signInWithGoogle": "使用 Google 登录",
|
||||
"signInWithGitHub": "使用 GitHub 登录",
|
||||
"termsOfService": "服务条款",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"byClickingContinue": "点击继续即表示您同意我们的",
|
||||
"and": "和"
|
||||
},
|
||||
"register": {
|
||||
"title": "注册",
|
||||
"createAccount": "创建账号",
|
||||
"name": "姓名",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"signUp": "注册",
|
||||
"signIn": "已经有账号?登录",
|
||||
"checkEmail": "请检查您的邮箱"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "忘记密码",
|
||||
"email": "邮箱",
|
||||
"send": "发送",
|
||||
"backToLogin": "返回登录",
|
||||
"checkEmail": "请检查您的邮箱"
|
||||
},
|
||||
"resetPassword": {
|
||||
"title": "重置密码",
|
||||
"password": "密码",
|
||||
"reset": "重置",
|
||||
"backToLogin": "返回登录"
|
||||
},
|
||||
"error": {
|
||||
"title": "哎呀!出错了!",
|
||||
"tryAgain": "请重试。",
|
||||
"backToLogin": "返回登录",
|
||||
"checkEmail": "请检查您的邮箱"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,20 +2,6 @@ import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import { withContentCollections } from "@content-collections/next";
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
|
||||
*
|
||||
* Explicitly specify the path to the request config file
|
||||
*/
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
// module.exports = {
|
||||
// experimental: {
|
||||
// // https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
|
||||
// missingSuspenseWithCSRBailout: false,
|
||||
// },
|
||||
// }
|
||||
|
||||
/**
|
||||
* https://nextjs.org/docs/app/api-reference/config/next-config-js
|
||||
*/
|
||||
@ -29,16 +15,11 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
|
||||
images: {
|
||||
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
|
||||
// vercel has limits on image optimization, 1000 images per month
|
||||
// unoptimized: true,
|
||||
|
||||
// https://medium.com/@niniroula/nextjs-upgrade-next-image-and-dangerouslyallowsvg-c934060d79f8
|
||||
// The requested resource "https://cdn.sanity.io/images/58a2mkbj/preview/xxx.svg?fit=max&auto=format" has type "image/svg+xml"
|
||||
// but dangerouslyAllowSVG is disabled
|
||||
// dangerouslyAllowSVG: true,
|
||||
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "localhost",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "avatars.githubusercontent.com",
|
||||
@ -55,6 +36,13 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* You can specify the path to the request config file or use the default one (@/i18n/request.ts)
|
||||
*
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
|
||||
*/
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/**
|
||||
* withContentCollections must be the outermost plugin
|
||||
*
|
||||
|
@ -7,14 +7,7 @@ import LogoCloud from "@/components/nsui/logo-cloud";
|
||||
import Pricing from "@/components/nsui/pricing";
|
||||
import StatsSection from "@/components/nsui/stats";
|
||||
import WallOfLoveSection from "@/components/nsui/testimonials";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "",
|
||||
canonicalUrl: `${siteConfig.url}/`,
|
||||
});
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
interface HomePageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
@ -23,21 +16,11 @@ interface HomePageProps {
|
||||
export default async function HomePage(props: HomePageProps) {
|
||||
const params = await props.params;
|
||||
const { locale } = params;
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Use getTranslations instead of useTranslations for async server components
|
||||
const t = await getTranslations('HomePage');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-12 flex flex-col gap-16">
|
||||
|
||||
<div className="text-center">
|
||||
<h1>{t('title')}</h1>
|
||||
</div>
|
||||
|
||||
<HeroSection />
|
||||
|
||||
<LogoCloud />
|
13
src/app/[locale]/(marketing)/layout.tsx
Normal file
13
src/app/[locale]/(marketing)/layout.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { marketingConfig } from "@/config/marketing";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Navbar } from "@/components/marketing/navbar";
|
||||
|
||||
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar scroll={true} config={marketingConfig} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* Catching unknown routes
|
||||
*
|
||||
* all requests that are matched within the [locale] segment will render
|
||||
* the not-found page when an unknown route is encountered (e.g. /en/unknown).
|
||||
*
|
||||
* https://next-intl.dev/docs/environments/error-files#catching-unknown-routes
|
||||
*/
|
||||
export default function CatchAllPage() {
|
||||
notFound();
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { LoginForm } from "@/components/auth/login-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { Routes } from "@/routes";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Login",
|
||||
@ -10,25 +12,26 @@ export const metadata = constructMetadata({
|
||||
});
|
||||
|
||||
const LoginPage = () => {
|
||||
const t = useTranslations("AuthPage.login");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<LoginForm />
|
||||
<div className="text-balance text-center text-xs text-muted-foreground">
|
||||
By clicking continue, you agree to our{" "}
|
||||
<a
|
||||
href="/terms"
|
||||
{t("byClickingContinue")}
|
||||
<Link
|
||||
href={Routes.TermsOfService}
|
||||
className="underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Terms of Service
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="/privacy"
|
||||
{t("termsOfService")}
|
||||
</Link>{" "}
|
||||
{t("and")}{" "}
|
||||
<Link
|
||||
href={Routes.PrivacyPolicy}
|
||||
className="underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
.
|
||||
{t("privacyPolicy")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -1,54 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
|
||||
|
||||
import {lazy} from "react";
|
||||
|
||||
/**
|
||||
* Learned how to recover from a server component error in Next.js from @asidorenko_
|
||||
*
|
||||
* https://x.com/asidorenko_/status/1841547623712407994
|
||||
* Move error content to a separate chunk and load it only when needed
|
||||
*
|
||||
* Note that error.tsx is loaded right after your app has initialized.
|
||||
* If your app is performance-sensitive and you want to avoid loading translation functionality
|
||||
* from next-intl as part of this bundle, you can export a lazy reference from your error file.
|
||||
* https://next-intl.dev/docs/environments/error-files#errorjs
|
||||
*/
|
||||
export default function ErrorPage({ reset }: { reset: () => void }) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-2xl text-center">Oops! Something went wrong!</h1>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
reset();
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
Try again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
Back to home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default lazy(() => import('@/components/error/error'));
|
||||
|
@ -8,7 +8,7 @@ import { cn } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import { Toaster } from 'sonner';
|
||||
@ -21,19 +21,13 @@ interface LocaleLayoutProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
// export function generateStaticParams() {
|
||||
// return routing.locales.map((locale) => ({ locale }));
|
||||
// }
|
||||
|
||||
// export async function generateMetadata(props: Omit<LocaleLayoutProps, 'children'>) {
|
||||
// const { locale } = await props.params;
|
||||
// const t = await getTranslations({ locale, namespace: 'LocaleLayout' });
|
||||
|
||||
// return {
|
||||
// title: t('title')
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* 1. Locale Layout
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#layout
|
||||
*
|
||||
* 2. NextIntlClientProvider
|
||||
* https://next-intl.dev/docs/usage/configuration#nextintlclientprovider
|
||||
*/
|
||||
export default async function LocaleLayout({ children, params }: LocaleLayoutProps) {
|
||||
const { locale } = await params;
|
||||
|
||||
@ -42,11 +36,7 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
// Providing all messages to the client side
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
@ -59,14 +49,9 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}>
|
||||
{/* https://next-intl.dev/docs/usage/configuration#nextintlclientprovider */}
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar scroll={true} config={marketingConfig} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
{children}
|
||||
|
||||
<Toaster richColors position="top-right" offset={64} />
|
||||
|
||||
|
@ -1,24 +1,30 @@
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
/**
|
||||
* Note that `app/[locale]/[...rest]/page.tsx`
|
||||
* is necessary for this page to render.
|
||||
*
|
||||
* https://next-intl.dev/docs/environments/error-files#not-foundjs
|
||||
* https://next-intl.dev/docs/environments/error-files#catching-non-localized-requests
|
||||
*/
|
||||
export default function NotFound() {
|
||||
const t = useTranslations('NotFoundPage');
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
<h1 className="text-4xl font-bold">{t('title')}</h1>
|
||||
|
||||
<p className="text-balance text-center text-xl font-medium px-4">
|
||||
Sorry, the page you are looking for does not exist.
|
||||
{t('message')}
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" variant="default">
|
||||
<Link href="/">Back to home</Link>
|
||||
<Link href="/">{t('backToHome')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,6 +7,8 @@ interface Props {
|
||||
/**
|
||||
* Since we have a `not-found.tsx` page on the root, a layout file
|
||||
* is required, even if it's just passing children through.
|
||||
*
|
||||
* https://next-intl.dev/docs/environments/error-files#catching-non-localized-requests
|
||||
*/
|
||||
export default function RootLayout({children}: Props) {
|
||||
return children;
|
||||
|
@ -3,9 +3,13 @@
|
||||
import Error from "next/error";
|
||||
|
||||
/**
|
||||
* Catching non-localized requests
|
||||
*
|
||||
* This page renders when a route like `/unknown.txt` is requested.
|
||||
* In this case, the layout at `app/[locale]/layout.tsx` receives
|
||||
* an invalid value as the `[locale]` param and calls `notFound()`.
|
||||
*
|
||||
* https://next-intl.dev/docs/environments/error-files#catching-non-localized-requests
|
||||
*/
|
||||
export default function GlobalNotFound() {
|
||||
return (
|
||||
|
@ -1,23 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
interface BottomButtonProps {
|
||||
href: string;
|
||||
href: string
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const BottomButton = ({ href, label }: BottomButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
className="font-normal w-full text-muted-foreground"
|
||||
size="sm"
|
||||
asChild
|
||||
<Link
|
||||
href={href as ComponentProps<typeof Link>["href"]}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "link", size: "sm" }),
|
||||
"font-normal w-full text-muted-foreground hover:underline underline-offset-4 hover:text-primary"
|
||||
)}
|
||||
>
|
||||
<a href={href} className="hover:underline underline-offset-4 hover:text-primary">
|
||||
{label}
|
||||
</a>
|
||||
</Button>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
@ -1,18 +1,21 @@
|
||||
import { AuthCard } from "@/components/auth/auth-card";
|
||||
import { Routes } from "@/routes";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const ErrorCard = () => {
|
||||
const t = useTranslations("AuthPage.error");
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
headerLabel="Something went wrong!"
|
||||
headerLabel={t("title")}
|
||||
bottomButtonHref={`${Routes.Login}`}
|
||||
bottomButtonLabel="Back to login"
|
||||
bottomButtonLabel={t("backToLogin")}
|
||||
className="border-none"
|
||||
>
|
||||
<div className="w-full flex justify-center items-center py-4 gap-2">
|
||||
<TriangleAlertIcon className="text-destructive size-4" />
|
||||
<p className="font-medium text-destructive">Please try again.</p>
|
||||
<p className="font-medium text-destructive">{t("tryAgain")}</p>
|
||||
</div>
|
||||
</AuthCard>
|
||||
);
|
||||
|
@ -21,11 +21,14 @@ import type * as z from "zod";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Routes } from "@/routes";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const ForgotPasswordForm = () => {
|
||||
export const ForgotPasswordForm = ({ className }: { className?: string }) => {
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const t = useTranslations("AuthPage.forgotPassword");
|
||||
|
||||
const form = useForm<z.infer<typeof ForgotPasswordSchema>>({
|
||||
resolver: zodResolver(ForgotPasswordSchema),
|
||||
@ -51,7 +54,7 @@ export const ForgotPasswordForm = () => {
|
||||
},
|
||||
onSuccess: (ctx) => {
|
||||
// console.log("forgotPassword, success:", ctx.data);
|
||||
setSuccess("Please check your email inbox");
|
||||
setSuccess(t("checkEmail"));
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log("forgotPassword, error:", ctx.error);
|
||||
@ -62,10 +65,10 @@ export const ForgotPasswordForm = () => {
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
headerLabel="Froget password?"
|
||||
bottomButtonLabel="Back to login"
|
||||
headerLabel={t("title")}
|
||||
bottomButtonLabel={t("backToLogin")}
|
||||
bottomButtonHref={`${Routes.Login}`}
|
||||
className="border-none"
|
||||
className={cn("border-none", className)}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
@ -75,7 +78,7 @@ export const ForgotPasswordForm = () => {
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t("email")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@ -102,7 +105,7 @@ export const ForgotPasswordForm = () => {
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<span>Send reset password email</span>
|
||||
<span>{t("send")}</span>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -14,11 +14,13 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { LoginSchema } from "@/lib/schemas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Routes } from "@/routes";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -28,6 +30,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl");
|
||||
const urlError = searchParams.get("error");
|
||||
const t = useTranslations("AuthPage.login");
|
||||
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
@ -74,8 +77,8 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
headerLabel="Welcome back"
|
||||
bottomButtonLabel="Don't have an account? Sign up"
|
||||
headerLabel={t("welcomeBack")}
|
||||
bottomButtonLabel={t("signUp")}
|
||||
bottomButtonHref={`${Routes.Register}`}
|
||||
showSocialLoginButton
|
||||
className={cn("border-none", className)}
|
||||
@ -88,7 +91,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t("email")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@ -107,19 +110,19 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex justify-between items-center">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t("password")}</FormLabel>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
asChild
|
||||
className="px-0 font-normal text-muted-foreground"
|
||||
>
|
||||
<a
|
||||
<Link
|
||||
href={`${Routes.ForgotPassword}`}
|
||||
className="text-xs hover:underline hover:underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
{t("forgotPassword")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<FormControl>
|
||||
@ -148,7 +151,7 @@ export const LoginForm = ({ className }: { className?: string }) => {
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<span>Sign in</span>
|
||||
<span>{t("signIn")}</span>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -18,6 +18,7 @@ import { authClient } from "@/lib/auth-client";
|
||||
import { RegisterSchema } from "@/lib/schemas";
|
||||
import { Routes } from "@/routes";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -26,6 +27,7 @@ import type * as z from "zod";
|
||||
export const RegisterForm = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl");
|
||||
const t = useTranslations("AuthPage.register");
|
||||
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
@ -64,7 +66,7 @@ export const RegisterForm = () => {
|
||||
onSuccess: (ctx) => {
|
||||
// sign up success, user information stored in ctx.data
|
||||
// console.log("register, success:", ctx.data);
|
||||
setSuccess('Please check your email for verification');
|
||||
setSuccess(t("checkEmail"));
|
||||
},
|
||||
onError: (ctx) => {
|
||||
// sign up fail, display the error message
|
||||
@ -76,8 +78,8 @@ export const RegisterForm = () => {
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
headerLabel="Create an account"
|
||||
bottomButtonLabel="Already have an account? Sign in"
|
||||
headerLabel={t("createAccount")}
|
||||
bottomButtonLabel={t("signIn")}
|
||||
bottomButtonHref={`${Routes.Login}`}
|
||||
showSocialLoginButton
|
||||
className="border-none"
|
||||
@ -90,7 +92,7 @@ export const RegisterForm = () => {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isPending} placeholder="name" />
|
||||
</FormControl>
|
||||
@ -103,7 +105,7 @@ export const RegisterForm = () => {
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t("email")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@ -121,7 +123,7 @@ export const RegisterForm = () => {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t("password")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@ -148,7 +150,7 @@ export const RegisterForm = () => {
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<span>Sign up</span>
|
||||
<span>{t("signUp")}</span>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -18,6 +18,7 @@ import { authClient } from "@/lib/auth-client";
|
||||
import { ResetPasswordSchema } from "@/lib/schemas";
|
||||
import { Routes } from "@/routes";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -32,10 +33,10 @@ export const ResetPasswordForm = () => {
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [error, setError] = useState<string | undefined>("");
|
||||
const [success, setSuccess] = useState<string | undefined>("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const t = useTranslations("AuthPage.resetPassword");
|
||||
|
||||
const form = useForm<z.infer<typeof ResetPasswordSchema>>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
@ -73,8 +74,8 @@ export const ResetPasswordForm = () => {
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
headerLabel="Reset password"
|
||||
bottomButtonLabel="Back to login"
|
||||
headerLabel={t("title")}
|
||||
bottomButtonLabel={t("backToLogin")}
|
||||
bottomButtonHref={`${Routes.Login}`}
|
||||
className="border-none"
|
||||
>
|
||||
@ -86,7 +87,7 @@ export const ResetPasswordForm = () => {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t("password")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@ -113,7 +114,7 @@ export const ResetPasswordForm = () => {
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<span>Reset password</span>
|
||||
<span>{t("reset")}</span>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
@ -8,6 +8,7 @@ import { GitHubIcon } from "@/components/icons/github";
|
||||
import { GoogleIcon } from "@/components/icons/google";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Routes } from "@/routes";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* social login buttons
|
||||
@ -16,6 +17,7 @@ export const SocialLoginButton = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl");
|
||||
const [isLoading, setIsLoading] = useState<"google" | "github" | null>(null);
|
||||
const t = useTranslations("AuthPage.login");
|
||||
|
||||
const onClick = async (provider: "google" | "github") => {
|
||||
await authClient.signIn.social({
|
||||
@ -76,7 +78,7 @@ export const SocialLoginButton = () => {
|
||||
) : (
|
||||
<GoogleIcon className="size-5 mr-2" />
|
||||
)}
|
||||
<span>Login with Google</span>
|
||||
<span>{t("signInWithGoogle")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
@ -90,7 +92,7 @@ export const SocialLoginButton = () => {
|
||||
) : (
|
||||
<GitHubIcon className="size-5 mr-2" />
|
||||
)}
|
||||
<span>Login with GitHub</span>
|
||||
<span>{t("signInWithGitHub")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
59
src/components/error/error.tsx
Normal file
59
src/components/error/error.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
|
||||
/**
|
||||
* 1. Note that error.tsx is loaded right after your app has initialized.
|
||||
* If your app is performance-sensitive and you want to avoid loading translation functionality
|
||||
* from next-intl as part of this bundle, you can export a lazy reference from your error file.
|
||||
* https://next-intl.dev/docs/environments/error-files#errorjs
|
||||
*
|
||||
* 2. Learned how to recover from a server component error in Next.js from @asidorenko_
|
||||
* https://x.com/asidorenko_/status/1841547623712407994
|
||||
*/
|
||||
export default function Error({ reset }: { reset: () => void }) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const t = useTranslations('ErrorPage');
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-2xl text-center">{t('title')}</h1>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
reset();
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{t('tryAgain')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
{t('backToHome')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -18,6 +18,15 @@ import { useLocale } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useTransition } from "react";
|
||||
|
||||
/**
|
||||
* 1. useRouter
|
||||
* By combining usePathname with useRouter, you can change the locale for the current page
|
||||
* programmatically by navigating to the same pathname, while overriding the locale.
|
||||
* Depending on if you’re using the pathnames setting, you optionally have to forward params
|
||||
* to potentially resolve an internal pathname.
|
||||
*
|
||||
* https://next-intl.dev/docs/routing/navigation#userouter
|
||||
*/
|
||||
export default function LocaleSelector() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@ -34,8 +43,42 @@ export default function LocaleSelector() {
|
||||
setCurrentLocale(nextLocale);
|
||||
|
||||
startTransition(() => {
|
||||
// // For root path, just use the string
|
||||
// if (pathname === "/") {
|
||||
// router.replace("/", { locale: nextLocale });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // For blog index
|
||||
// if (pathname === "/blog") {
|
||||
// router.replace("/blog", { locale: nextLocale });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // For dynamic routes, reconstruct with the correct params
|
||||
// if (pathname.startsWith("/blog/")) {
|
||||
// if (pathname.includes("category")) {
|
||||
// router.replace({
|
||||
// pathname: "/blog/category/[slug]",
|
||||
// params: { slug: params.slug as string }
|
||||
// }, { locale: nextLocale });
|
||||
// } else {
|
||||
// router.replace({
|
||||
// pathname: "/blog/[...slug]",
|
||||
// params: { slug: Array.isArray(params.slug) ? params.slug : [params.slug as string] }
|
||||
// }, { locale: nextLocale });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Fallback for other routes
|
||||
// router.replace("/", { locale: nextLocale });
|
||||
|
||||
router.replace(
|
||||
{ pathname }, // if your want to redirect to the current page
|
||||
// @ts-expect-error -- TypeScript will validate that only known `params`
|
||||
// are used in combination with a given `pathname`. Since the two will
|
||||
// always match for the current route, we can skip runtime checks.
|
||||
{ pathname, params },
|
||||
{ locale: nextLocale }
|
||||
);
|
||||
});
|
||||
|
@ -7,6 +7,8 @@ import { ComponentProps } from "react";
|
||||
|
||||
/**
|
||||
* This component is used to render a link in the navigation bar.
|
||||
*
|
||||
* https://next-intl.dev/docs/routing/navigation#link
|
||||
*/
|
||||
export default function NavigationLink({
|
||||
href,
|
@ -1,11 +1,11 @@
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
export const LOCALES = ["en", "zh"];
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
export const LOCALE_LIST: Record<string, string> = {
|
||||
en: "🇬🇧 English",
|
||||
zh: "🇨🇳 中文",
|
||||
};
|
||||
export const LOCALES = Object.keys(LOCALE_LIST);
|
||||
|
||||
/**
|
||||
* Next.js internationalized routing
|
||||
@ -27,6 +27,15 @@ export const routing = defineRouting({
|
||||
// https://next-intl.dev/docs/routing#pathnames
|
||||
pathnames: {
|
||||
"/": "/",
|
||||
"/blog": "/blog",
|
||||
"/blog/[...slug]": "/blog/[...slug]",
|
||||
"/blog/category/[slug]": "/blog/category/[slug]",
|
||||
// 认证相关路径
|
||||
"/auth/login": "/auth/login",
|
||||
"/auth/register": "/auth/register",
|
||||
"/auth/forgot-password": "/auth/forgot-password",
|
||||
"/auth/reset-password": "/auth/reset-password",
|
||||
"/auth/error": "/auth/error",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -7,4 +7,4 @@ export const authClient = createAuthClient({
|
||||
// https://www.better-auth.com/docs/plugins/admin#add-the-client-plugin
|
||||
adminClient(),
|
||||
]
|
||||
})
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ export enum Routes {
|
||||
Pricing = '/#pricing',
|
||||
FAQ = '/#faq',
|
||||
|
||||
TermsOfUse = '/terms-of-use',
|
||||
TermsOfService = '/terms-of-service',
|
||||
PrivacyPolicy = '/privacy-policy',
|
||||
CookiePolicy = '/cookie-policy',
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user