chore: bump version to 2.20.1 in package.json and refactor layout and… (#404)

* chore: bump version to 2.20.1 in package.json and refactor layout and form components

- Incremented application version from 2.20.0 to 2.20.1 in package.json.
- Refactored RootLayout to optimize asynchronous calls and introduced getRootClassName function for better class management.
- Updated font handling in getFontsClassName function to streamline class generation.
- Enhanced various authentication form components by replacing Input with EmailInput and PasswordInput for improved consistency and usability.
- Adjusted layout styles in AuthLayoutShell and other components for better responsiveness.

* fix: improve content rendering fallback logic in ContentRenderer component

- Enhanced the ContentRenderer function to explicitly check for the presence of a renderer before returning content.
- Added a fallback mechanism to return raw content as React nodes when no renderer is found, improving robustness and user experience.
This commit is contained in:
Giancarlo Buomprisco
2025-11-02 16:14:21 +07:00
committed by GitHub
parent 116d41a284
commit ac12c9355c
14 changed files with 248 additions and 226 deletions

View File

@@ -1,6 +1,7 @@
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import { Toaster } from '@kit/ui/sonner'; import { Toaster } from '@kit/ui/sonner';
import { cn } from '@kit/ui/utils';
import { RootProviders } from '~/components/root-providers'; import { RootProviders } from '~/components/root-providers';
import { getFontsClassName } from '~/lib/fonts'; import { getFontsClassName } from '~/lib/fonts';
@@ -19,13 +20,17 @@ export default async function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { language } = await createI18nServerInstance(); const [theme, nonce, i18n] = await Promise.all([
const theme = await getRootTheme(); getRootTheme(),
const className = getFontsClassName(theme); getCspNonce(),
const nonce = await getCspNonce(); createI18nServerInstance(),
]);
const className = getRootClassName(theme);
const language = i18n.language;
return ( return (
<html lang={language} className={`${className} overscroll-y-none`}> <html lang={language} className={className}>
<body> <body>
<RootProviders theme={theme} lang={language} nonce={nonce}> <RootProviders theme={theme} lang={language} nonce={nonce}>
{children} {children}
@@ -37,6 +42,15 @@ export default async function RootLayout({
); );
} }
function getRootClassName(theme: string) {
const fontsClassName = getFontsClassName(theme);
return cn(
'bg-background min-h-screen overscroll-y-none antialiased',
fontsClassName,
);
}
async function getCspNonce() { async function getCspNonce() {
const headersStore = await headers(); const headersStore = await headers();

View File

@@ -42,7 +42,7 @@ export function getFontsClassName(theme?: string) {
[], [],
); );
return cn('bg-background min-h-screen antialiased', ...font, { return cn(...font, {
dark, dark,
light, light,
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-supabase-saas-kit-turbo", "name": "next-supabase-saas-kit-turbo",
"version": "2.20.0", "version": "2.20.1",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {

View File

@@ -1,4 +1,5 @@
import type { CmsType } from '@kit/cms-types'; import { CmsType } from '@kit/cms-types';
import { createRegistry } from '@kit/shared/registry';
const CMS_CLIENT = process.env.CMS_CLIENT as CmsType; const CMS_CLIENT = process.env.CMS_CLIENT as CmsType;
@@ -7,42 +8,34 @@ interface ContentRendererProps {
type?: CmsType; type?: CmsType;
} }
// Create a registry for CMS client implementations
const cmsContentRendererRegistry = createRegistry<
React.ComponentType<ContentRendererProps>,
CmsType
>();
export async function ContentRenderer({ export async function ContentRenderer({
content, content,
type = CMS_CLIENT, type = CMS_CLIENT,
}: ContentRendererProps) { }: ContentRendererProps) {
const Renderer = await getContentRenderer(type); const Renderer = await cmsContentRendererRegistry.get(type);
return Renderer ? <Renderer content={content} /> : null; if (Renderer) {
} return <Renderer content={content} />;
/**
* Gets the content renderer for the specified CMS client.
*
* @param {CmsType} type - The type of CMS client.
*/
async function getContentRenderer(type: CmsType) {
switch (type) {
case 'keystatic': {
const { KeystaticContentRenderer } = await import(
'@kit/keystatic/renderer'
);
return KeystaticContentRenderer;
}
case 'wordpress': {
const { WordpressContentRenderer } = await import(
'@kit/wordpress/renderer'
);
return WordpressContentRenderer;
}
default: {
console.error(`Unknown CMS client: ${type as string}`);
return null;
}
} }
// fallback to the raw content if no renderer is found
return content as React.ReactNode;
} }
cmsContentRendererRegistry.register('keystatic', async () => {
const { KeystaticContentRenderer } = await import('@kit/keystatic/renderer');
return KeystaticContentRenderer;
});
cmsContentRendererRegistry.register('wordpress', async () => {
const { WordpressContentRenderer } = await import('@kit/wordpress/renderer');
return WordpressContentRenderer;
});

View File

@@ -21,7 +21,7 @@ export function AuthLayoutShell({
<div <div
className={cn( className={cn(
'bg-background flex w-full max-w-[23rem] flex-col gap-y-6 rounded-lg px-6 md:w-8/12 md:px-8 md:py-6 lg:w-5/12 lg:px-8 xl:w-4/12 xl:py-8', 'bg-background flex w-full max-w-[22rem] flex-col gap-y-6 rounded-lg px-6 md:w-8/12 md:px-8 md:py-6 lg:w-5/12 lg:px-8 xl:w-4/12 xl:py-8',
contentClassName, contentClassName,
)} )}
> >

View File

@@ -0,0 +1,30 @@
'use client';
import { Mail } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from '@kit/ui/input-group';
export function EmailInput(props: React.ComponentProps<'input'>) {
const { t } = useTranslation('auth');
return (
<InputGroup className="dark:bg-background">
<InputGroupAddon>
<Mail className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
data-test={'email-input'}
required
type="email"
placeholder={t('emailPlaceholder')}
{...props}
/>
</InputGroup>
);
}

View File

@@ -70,11 +70,7 @@ export function ExistingAccountHintImpl({
return ( return (
<If condition={Boolean(methodDescription)}> <If condition={Boolean(methodDescription)}>
<Alert <Alert data-test={'existing-account-hint'} className={className}>
data-test={'existing-account-hint'}
variant="info"
className={className}
>
<UserCheck className="h-4 w-4" /> <UserCheck className="h-4 w-4" />
<AlertDescription> <AlertDescription>
@@ -84,10 +80,7 @@ export function ExistingAccountHintImpl({
components={{ components={{
method: <span className="font-medium" />, method: <span className="font-medium" />,
signInLink: ( signInLink: (
<Link <Link href={signInPath} className="font-medium underline" />
href={signInPath}
className="font-medium underline hover:no-underline"
/>
), ),
}} }}
/> />

View File

@@ -19,12 +19,12 @@ import {
FormMessage, FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner'; import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { useCaptcha } from '../captcha/client'; import { useCaptcha } from '../captcha/client';
import { useLastAuthMethod } from '../hooks/use-last-auth-method'; import { useLastAuthMethod } from '../hooks/use-last-auth-method';
import { EmailInput } from './email-input';
import { TermsAndConditionsFormField } from './terms-and-conditions-form-field'; import { TermsAndConditionsFormField } from './terms-and-conditions-form-field';
export function MagicLinkAuthContainer({ export function MagicLinkAuthContainer({
@@ -118,13 +118,7 @@ export function MagicLinkAuthContainer({
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <EmailInput data-test="email-input" {...field} />
data-test={'email-input'}
required
type="email"
placeholder={t('auth:emailPlaceholder')}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -17,7 +17,6 @@ import {
FormItem, FormItem,
FormMessage, FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { import {
InputOTP, InputOTP,
InputOTPGroup, InputOTPGroup,
@@ -30,6 +29,7 @@ import { Trans } from '@kit/ui/trans';
import { useCaptcha } from '../captcha/client'; import { useCaptcha } from '../captcha/client';
import { useLastAuthMethod } from '../hooks/use-last-auth-method'; import { useLastAuthMethod } from '../hooks/use-last-auth-method';
import { AuthErrorAlert } from './auth-error-alert'; import { AuthErrorAlert } from './auth-error-alert';
import { EmailInput } from './email-input';
const EmailSchema = z.object({ email: z.string().email() }); const EmailSchema = z.object({ email: z.string().email() });
const OtpSchema = z.object({ token: z.string().min(6).max(6) }); const OtpSchema = z.object({ token: z.string().min(6).max(6) });
@@ -216,13 +216,7 @@ function OtpEmailForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <EmailInput data-test="otp-email-input" {...field} />
required
type="email"
placeholder="email@example.com"
data-test="otp-email-input"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -0,0 +1,46 @@
'use client';
import { useState } from 'react';
import { Eye, EyeOff, Lock } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from '@kit/ui/input-group';
export function PasswordInput(props: React.ComponentProps<'input'>) {
const [showPassword, setShowPassword] = useState(false);
return (
<InputGroup className="dark:bg-background">
<InputGroupAddon>
<Lock className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
data-test="password-input"
type={showPassword ? 'text' : 'password'}
placeholder={'************'}
{...props}
/>
<InputGroupAddon align="inline-end">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</InputGroupAddon>
</InputGroup>
);
}

View File

@@ -3,7 +3,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { ArrowRight } from 'lucide-react'; import { ArrowRight, Mail } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { z } from 'zod'; import type { z } from 'zod';
@@ -14,14 +14,18 @@ import {
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel,
FormMessage, FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input'; import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from '@kit/ui/input-group';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { PasswordSignInSchema } from '../schemas/password-sign-in.schema'; import { PasswordSignInSchema } from '../schemas/password-sign-in.schema';
import { PasswordInput } from './password-input';
export function PasswordSignInForm({ export function PasswordSignInForm({
onSubmit, onSubmit,
@@ -48,67 +52,61 @@ export function PasswordSignInForm({
className={'flex w-full flex-col gap-y-4'} className={'flex w-full flex-col gap-y-4'}
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
> >
<FormField <div className={'flex flex-col space-y-2.5'}>
control={form.control} <FormField
name={'email'} control={form.control}
render={({ field }) => ( name={'email'}
<FormItem> render={({ field }) => (
<FormLabel> <FormItem>
<Trans i18nKey={'common:emailAddress'} /> <FormControl>
</FormLabel> <InputGroup className="dark:bg-background">
<InputGroupAddon>
<Mail className="h-4 w-4" />
</InputGroupAddon>
<FormControl> <InputGroupInput
<Input data-test={'email-input'}
data-test={'email-input'} required
required type="email"
type="email" placeholder={t('emailPlaceholder')}
placeholder={t('emailPlaceholder')} {...field}
{...field} />
/> </InputGroup>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name={'password'} name={'password'}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormControl>
<Trans i18nKey={'common:password'} /> <PasswordInput {...field} />
</FormLabel> </FormControl>
<FormControl> <FormMessage />
<Input
required
data-test={'password-input'}
type="password"
placeholder={''}
{...field}
/>
</FormControl>
<FormMessage /> <div>
<Button
<div> asChild
<Button type={'button'}
asChild size={'sm'}
type={'button'} variant={'link'}
size={'sm'} className={'text-xs'}
variant={'link'} >
className={'text-xs'} <Link href={'/auth/password-reset'}>
> <Trans i18nKey={'auth:passwordForgottenQuestion'} />
<Link href={'/auth/password-reset'}> </Link>
<Trans i18nKey={'auth:passwordForgottenQuestion'} /> </Button>
</Link> </div>
</Button> </FormItem>
</div> )}
</FormItem> />
)} </div>
/>
<Button <Button
data-test="auth-submit-button" data-test="auth-submit-button"

View File

@@ -3,7 +3,6 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
@@ -12,14 +11,14 @@ import {
FormDescription, FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel,
FormMessage, FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { PasswordSignUpSchema } from '../schemas/password-sign-up.schema'; import { PasswordSignUpSchema } from '../schemas/password-sign-up.schema';
import { EmailInput } from './email-input';
import { PasswordInput } from './password-input';
import { TermsAndConditionsFormField } from './terms-and-conditions-form-field'; import { TermsAndConditionsFormField } from './terms-and-conditions-form-field';
interface PasswordSignUpFormProps { interface PasswordSignUpFormProps {
@@ -43,8 +42,6 @@ export function PasswordSignUpForm({
onSubmit, onSubmit,
loading, loading,
}: PasswordSignUpFormProps) { }: PasswordSignUpFormProps) {
const { t } = useTranslation();
const form = useForm({ const form = useForm({
resolver: zodResolver(PasswordSignUpSchema), resolver: zodResolver(PasswordSignUpSchema),
defaultValues: { defaultValues: {
@@ -60,82 +57,56 @@ export function PasswordSignUpForm({
className={'flex w-full flex-col gap-y-4'} className={'flex w-full flex-col gap-y-4'}
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
> >
<FormField <div className={'flex flex-col space-y-2.5'}>
control={form.control} <FormField
name={'email'} control={form.control}
render={({ field }) => ( name={'email'}
<FormItem> render={({ field }) => (
<FormLabel> <FormItem>
<Trans i18nKey={'common:emailAddress'} /> <FormControl>
</FormLabel> <EmailInput data-test={'email-input'} {...field} />
</FormControl>
<FormControl> <FormMessage />
<Input </FormItem>
data-test={'email-input'} )}
required />
type="email"
placeholder={t('emailPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage /> <FormField
</FormItem> control={form.control}
)} name={'password'}
/> render={({ field }) => (
<FormItem>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormField <FormMessage />
control={form.control} </FormItem>
name={'password'} )}
render={({ field }) => ( />
<FormItem>
<FormLabel>
<Trans i18nKey={'common:password'} />
</FormLabel>
<FormControl> <FormField
<Input control={form.control}
required name={'repeatPassword'}
data-test={'password-input'} render={({ field }) => (
type="password" <FormItem>
autoComplete="new-password" <FormControl>
placeholder={''} <PasswordInput
{...field} data-test={'repeat-password-input'}
/> {...field}
</FormControl> />
</FormControl>
<FormMessage /> <FormDescription>
</FormItem> <Trans i18nKey={'auth:repeatPasswordDescription'} />
)} </FormDescription>
/>
<FormField <FormMessage />
control={form.control} </FormItem>
name={'repeatPassword'} )}
render={({ field }) => ( />
<FormItem> </div>
<FormLabel>
<Trans i18nKey={'auth:repeatPassword'} />
</FormLabel>
<FormControl>
<Input
required
data-test={'repeat-password-input'}
type="password"
placeholder={''}
{...field}
/>
</FormControl>
<FormMessage />
<FormDescription className={'pb-2 text-xs'}>
<Trans i18nKey={'auth:repeatPasswordHint'} />
</FormDescription>
</FormItem>
)}
/>
<If condition={displayTermsCheckbox}> <If condition={displayTermsCheckbox}>
<TermsAndConditionsFormField /> <TermsAndConditionsFormField />

View File

@@ -13,12 +13,12 @@ import {
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { useCaptcha } from '../captcha/client'; import { useCaptcha } from '../captcha/client';
import { EmailInput } from './email-input';
export function ResendAuthLinkForm(props: { export function ResendAuthLinkForm(props: {
redirectPath?: string; redirectPath?: string;
@@ -71,20 +71,18 @@ export function ResendAuthLinkForm(props: {
{captcha.field} {captcha.field}
<FormField <FormField
name={'email'}
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<FormLabel>
<Trans i18nKey={'common:emailAddress'} />
</FormLabel>
<FormControl> <FormControl>
<Input type="email" required {...field} /> <EmailInput data-test="email-input" {...field} />
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
); );
}} }}
name={'email'}
/> />
<Button disabled={resendLink.isPending}> <Button disabled={resendLink.isPending}>

View File

@@ -15,16 +15,16 @@ import { Button } from '@kit/ui/button';
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel,
FormMessage, FormMessage,
} from '@kit/ui/form'; } from '@kit/ui/form';
import { Heading } from '@kit/ui/heading'; import { Heading } from '@kit/ui/heading';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { PasswordResetSchema } from '../schemas/password-reset.schema'; import { PasswordResetSchema } from '../schemas/password-reset.schema';
import { PasswordInput } from './password-input';
export function UpdatePasswordForm(params: { export function UpdatePasswordForm(params: {
redirectTo: string; redirectTo: string;
@@ -72,22 +72,13 @@ export function UpdatePasswordForm(params: {
toast.success(t('account:updatePasswordSuccessMessage')); toast.success(t('account:updatePasswordSuccessMessage'));
})} })}
> >
<div className={'flex-col space-y-4'}> <div className={'flex-col space-y-2.5'}>
<FormField <FormField
name={'password'} name={'password'}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>
<Trans i18nKey={'common:password'} />
</FormLabel>
<FormControl> <FormControl>
<Input <PasswordInput autoComplete={'new-password'} {...field} />
required
type="password"
autoComplete={'new-password'}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -99,14 +90,14 @@ export function UpdatePasswordForm(params: {
name={'repeatPassword'} name={'repeatPassword'}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>
<Trans i18nKey={'common:repeatPassword'} />
</FormLabel>
<FormControl> <FormControl>
<Input required type="password" {...field} /> <PasswordInput autoComplete={'new-password'} {...field} />
</FormControl> </FormControl>
<FormDescription>
<Trans i18nKey={'common:repeatPassword'} />
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}