feat: refactor authentication translations and improve loading indicators

- Moved common authentication messages to a new `common` namespace in English and Chinese translation files for better organization.
- Updated `LoginPage`, `RegisterPage`, and various form components to utilize the new translation keys.
- Enhanced loading indicators in `ForgotPasswordForm`, `LoginForm`, `RegisterForm`, and `ResetPasswordForm` components for improved user feedback.
- Refactored `SocialLoginButton` and `NavbarMobile` components to utilize the current user context for better user experience.
This commit is contained in:
javayhu 2025-04-04 19:56:43 +08:00
parent 043016e08d
commit db7d86851a
14 changed files with 85 additions and 51 deletions

View File

@ -149,10 +149,6 @@
"signInWithGitHub": "Sign In with GitHub",
"showPassword": "Show password",
"hidePassword": "Hide password",
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"byClickingContinue": "By clicking continue, you agree to our ",
"and": " and ",
"or": "Or continue with"
},
"register": {
@ -187,6 +183,12 @@
"tryAgain": "Please try again.",
"backToLogin": "Back to login",
"checkEmail": "Please check your email inbox"
},
"common": {
"termsOfService": "Terms of Service",
"privacyPolicy": "Privacy Policy",
"byClickingContinue": "By clicking continue, you agree to our ",
"and": " and "
}
},
"BlogPage": {

View File

@ -145,11 +145,7 @@
"signInWithGitHub": "使用 GitHub 登录",
"showPassword": "显示密码",
"hidePassword": "隐藏密码",
"termsOfService": "服务条款",
"privacyPolicy": "隐私政策",
"byClickingContinue": "继续即表示您同意我们的 ",
"and": " 和 ",
"or": "或"
"or": "或"
},
"register": {
"title": "注册",
@ -183,6 +179,12 @@
"tryAgain": "请重试",
"backToLogin": "返回登录",
"checkEmail": "请检查您的邮箱"
},
"common": {
"termsOfService": "服务条款",
"privacyPolicy": "隐私政策",
"byClickingContinue": "继续即表示您同意我们的 ",
"and": " 和 "
}
},
"BlogPage": {

View File

@ -24,7 +24,7 @@ export async function generateMetadata({
}
export default async function LoginPage() {
const t = await getTranslations('AuthPage.login');
const t = await getTranslations('AuthPage.common');
return (
<div className="flex flex-col gap-4">

View File

@ -1,6 +1,8 @@
import { RegisterForm } from '@/components/auth/register-form';
import { LocaleLink } from '@/i18n/navigation';
import { constructMetadata } from '@/lib/metadata';
import { getBaseUrlWithLocale } from '@/lib/urls/get-base-url';
import { Routes } from '@/routes';
import { Metadata } from 'next';
import { Locale } from 'next-intl';
import { getTranslations } from 'next-intl/server';
@ -10,10 +12,10 @@ export async function generateMetadata({
}: {
params: Promise<{ locale: Locale }>;
}): Promise<Metadata | undefined> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'Metadata'});
const pt = await getTranslations({locale, namespace: 'AuthPage.register'});
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Metadata' });
const pt = await getTranslations({ locale, namespace: 'AuthPage.register' });
return constructMetadata({
title: pt('title') + ' | ' + t('title'),
description: t('description'),
@ -22,5 +24,27 @@ export async function generateMetadata({
}
export default async function RegisterPage() {
return <RegisterForm />;
const t = await getTranslations('AuthPage.common');
return (
<div className="flex flex-col gap-4">
<RegisterForm />
<div className="text-balance text-center text-xs text-muted-foreground">
{t('byClickingContinue')}
<LocaleLink
href={Routes.TermsOfService}
className="underline underline-offset-4 hover:text-primary"
>
{t('termsOfService')}
</LocaleLink>{' '}
{t('and')}{' '}
<LocaleLink
href={Routes.PrivacyPolicy}
className="underline underline-offset-4 hover:text-primary"
>
{t('privacyPolicy')}
</LocaleLink>
</div>
</div>
);
}

View File

@ -113,10 +113,8 @@ export const ForgotPasswordForm = ({ className }: { className?: string }) => {
type="submit"
className="w-full flex items-center justify-center gap-2"
>
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
''
{isPending && (
<Icons.spinner className="mr-2 size-4 animate-spin" />
)}
<span>{t('send')}</span>
</Button>

View File

@ -175,10 +175,8 @@ export const LoginForm = ({ className }: { className?: string }) => {
type="submit"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
''
{isPending && (
<Icons.spinner className="mr-2 size-4 animate-spin" />
)}
<span>{t('signIn')}</span>
</Button>

View File

@ -174,10 +174,8 @@ export const RegisterForm = () => {
type="submit"
className="cursor-pointer w-full flex items-center justify-center gap-2"
>
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
''
{isPending && (
<Icons.spinner className="mr-2 size-4 animate-spin" />
)}
<span>{t('signUp')}</span>
</Button>

View File

@ -148,10 +148,8 @@ export const ResetPasswordForm = () => {
type="submit"
className="w-full flex items-center justify-center gap-2"
>
{isPending ? (
<Icons.spinner className="w-4 h-4 animate-spin" />
) : (
''
{isPending && (
<Icons.spinner className="mr-2 size-4 animate-spin" />
)}
<span>{t('reset')}</span>
</Button>

View File

@ -15,10 +15,10 @@ import { DividerWithText } from '@/components/auth/divider-with-text';
* social login buttons
*/
export const SocialLoginButton = () => {
const t = useTranslations('AuthPage.login');
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(

View File

@ -10,8 +10,8 @@ import {
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { getMenuLinks } from '@/config';
import { useCurrentUser } from '@/hooks/use-current-user';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { Portal } from '@radix-ui/react-portal';
@ -24,19 +24,18 @@ import {
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import * as React from 'react';
import { useEffect } from 'react';
import { RemoveScroll } from 'react-remove-scroll';
import { UserButton } from './user-button';
import { useEffect } from 'react';
export function NavbarMobile({
className,
...other
}: React.HTMLAttributes<HTMLDivElement>) {
const t = useTranslations();
const [open, setOpen] = React.useState<boolean>(false);
const localePathname = useLocalePathname();
const { data: session, error } = authClient.useSession();
const user = session?.user;
const t = useTranslations();
const currentUser = useCurrentUser();
useEffect(() => {
const handleRouteChangeStart = () => {
@ -80,10 +79,10 @@ export function NavbarMobile({
</span>
</LocaleLink>
{/* navbar right shows menu icon */}
{/* navbar right shows menu icon and user button */}
<div className="flex items-center justify-end gap-4">
{/* show user button if user is logged in */}
{user ? <UserButton user={user} /> : null}
{currentUser ? <UserButton user={currentUser} /> : null}
<Button
variant="ghost"
@ -110,7 +109,7 @@ export function NavbarMobile({
page will scroll when we scroll the mobile menu */}
<RemoveScroll allowPinchZoom enabled>
<MainMobileMenu
userLoggedIn={!!user}
userLoggedIn={!!currentUser}
onLinkClicked={handleToggleMobileMenu}
/>
</RemoveScroll>
@ -267,6 +266,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
>
{subItem.title}
</span>
{/* hide description for now */}
{/* {subItem.description && (
<p
className={cn(

View File

@ -21,7 +21,9 @@ interface CheckoutButtonProps {
* Checkout Button
*
* This client component creates a Stripe checkout session and redirects to it
* It's used to initiate the checkout process for a specific plan and price
* It's used to initiate the checkout process for a specific plan and price.
*
* NOTICE: Login is required when using this button.
*/
export function CheckoutButton({
planId,
@ -71,7 +73,7 @@ export function CheckoutButton({
>
{isLoading ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
<Loader2Icon className="mr-2 size-4 animate-spin" />
{t('loading')}
</>
) : (

View File

@ -69,7 +69,7 @@ export function CustomerPortalButton({
>
{isLoading ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
<Loader2Icon className="mr-2 size-4 animate-spin" />
{t('loading')}
</>
) : (

View File

@ -2,6 +2,7 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useCurrentUser } from '@/hooks/use-current-user';
import { formatPrice } from '@/lib/formatter';
import { cn } from '@/lib/utils';
import { PaymentType, PaymentTypes, PlanInterval, PlanIntervals, Price, PricePlan } from '@/payment/types';
@ -59,6 +60,7 @@ export function PricingCard({
}: PricingCardProps) {
const t = useTranslations('PricingPage.PricingCard');
const price = getPriceForPlan(plan, interval, paymentType);
const currentUser = useCurrentUser();
// generate formatted price and price label
let formattedPrice = '';
@ -115,18 +117,24 @@ export function PricingCard({
<CardDescription className="text-sm">{plan.description}</CardDescription>
{plan.isFree ? (
<LoginWrapper mode="modal" asChild>
<Button variant="outline" className="mt-4 w-full cursor-pointer">
currentUser ? (
<Button variant="outline" className="mt-4 w-full disabled">
{t('getStartedForFree')}
</Button>
</LoginWrapper>
) : (
<LoginWrapper mode="modal" asChild>
<Button variant="outline" className="mt-4 w-full cursor-pointer">
{t('getStartedForFree')}
</Button>
</LoginWrapper>
)
) : isCurrentPlan ? (
<Button disabled className="mt-4 w-full bg-blue-100 dark:bg-blue-800
text-blue-700 dark:text-blue-100 hover:bg-blue-100 dark:hover:bg-blue-800 border border-blue-200 dark:border-blue-700">
{t('yourCurrentPlan')}
</Button>
) : isPaidPlan ? (
<LoginWrapper mode="modal" asChild>
currentUser ? (
<CheckoutButton
planId={plan.id}
priceId={price.productId}
@ -135,7 +143,13 @@ export function PricingCard({
>
{plan.isLifetime ? t('getLifetimeAccess') : t('getStarted')}
</CheckoutButton>
</LoginWrapper>
) : (
<LoginWrapper mode="modal" asChild>
<Button variant="default" className="mt-4 w-full cursor-pointer">
{t('getStarted')}
</Button>
</LoginWrapper>
)
) : (
<Button disabled className="mt-4 w-full">
{t('notAvailable')}

View File

@ -182,9 +182,7 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
<FormControl>
<div className="relative flex items-center">
{isLoading && (
<div className="mr-2">
<Loader2Icon className="h-4 w-4 animate-spin text-primary" />
</div>
<Loader2Icon className="mr-2 size-4 animate-spin text-primary" />
)}
<Switch
checked={field.value}