From 98877aba2028cc1c4e4c0e41d69d11108b2e39e0 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Fri, 8 Aug 2025 12:49:06 +0200 Subject: [PATCH 1/6] feat: Add forgot password option --- apps/api/package.json | 1 + apps/api/src/locales/@vitnode/core/en.json | 8 ++- apps/docs/content/docs/ui/auto-form.mdx | 55 ++++++++++++++++++- apps/docs/package.json | 1 + .../login/password_reset/page.tsx | 25 +++++++++ apps/docs/src/examples/auto-form.tsx | 2 +- apps/docs/src/examples/input.tsx | 1 - apps/docs/src/locales/@vitnode/core/en.json | 8 ++- .../src/create/create-package-json.ts | 10 ++-- .../src/app/login/password_reset/page.tsx | 25 +++++++++ .../vitnode/src/components/form/auto-form.tsx | 9 ++- .../src/components/form/common/label.tsx | 22 +++++++- .../src/components/form/fields/checkbox.tsx | 5 +- .../components/form/fields/combobox-async.tsx | 7 ++- .../src/components/form/fields/combobox.tsx | 7 ++- .../src/components/form/fields/input.tsx | 7 ++- .../components/form/fields/radio-group.tsx | 7 ++- .../src/components/form/fields/select.tsx | 7 ++- .../src/components/form/fields/switch.tsx | 7 ++- .../src/components/form/fields/textarea.tsx | 7 ++- packages/vitnode/src/components/ui/button.tsx | 2 +- packages/vitnode/src/components/ui/card.tsx | 2 +- packages/vitnode/src/emails/ui/card.tsx | 5 +- packages/vitnode/src/locales/en.json | 8 ++- .../admin/sign-in/sign-in-admin-view.tsx | 2 +- .../views/auth/password-reset/form/form.tsx | 32 +++++++++++ .../auth/password-reset/form/use-form.ts | 14 +++++ .../password-reset/password-reset-view.tsx | 43 +++++++++++++++ .../src/views/auth/sign-in/form/form.tsx | 21 ++++++- .../src/views/auth/sign-in/sign-in-view.tsx | 12 ++-- .../components/password-input.tsx | 0 .../src/views/auth/sign-up/form/form.tsx | 4 +- .../src/views/auth/sign-up/sign-up-view.tsx | 2 +- .../src/views/layouts/theme/header/header.tsx | 2 +- pnpm-lock.yaml | 14 +++-- 35 files changed, 343 insertions(+), 41 deletions(-) create mode 100644 apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/password_reset/page.tsx create mode 100644 packages/vitnode/src/app/login/password_reset/page.tsx create mode 100644 packages/vitnode/src/views/auth/password-reset/form/form.tsx create mode 100644 packages/vitnode/src/views/auth/password-reset/form/use-form.ts create mode 100644 packages/vitnode/src/views/auth/password-reset/password-reset-view.tsx rename packages/vitnode/src/views/auth/{ => sign-up}/components/password-input.tsx (100%) diff --git a/apps/api/package.json b/apps/api/package.json index f66e10ede..51bdc7e05 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -25,6 +25,7 @@ "next-intl": "^4.3.4", "react": "^19.1.1", "react-dom": "^19.1.1", + "use-intl": "^4.3.4", "zod": "^4.0.14" }, "devDependencies": { diff --git a/apps/api/src/locales/@vitnode/core/en.json b/apps/api/src/locales/@vitnode/core/en.json index bec6c674c..6fe36226b 100644 --- a/apps/api/src/locales/@vitnode/core/en.json +++ b/apps/api/src/locales/@vitnode/core/en.json @@ -132,7 +132,8 @@ }, "password": { "label": "Password", - "required": "Password is required." + "required": "Password is required.", + "reset": "Forgot password?" }, "errors": { "access_denied": { @@ -141,6 +142,11 @@ } }, "submit": "Login" + }, + "password_reset": { + "title": "Reset Password", + "desc": "Please enter your email address below to receive a password reset link.", + "submit": "Send Reset Link" } } }, diff --git a/apps/docs/content/docs/ui/auto-form.mdx b/apps/docs/content/docs/ui/auto-form.mdx index bfe6cf10b..f1f721afc 100644 --- a/apps/docs/content/docs/ui/auto-form.mdx +++ b/apps/docs/content/docs/ui/auto-form.mdx @@ -47,9 +47,7 @@ const formSchema = z.object({ }, { id: 'email', - component: props => ( - - ), + component: props => , }, { id: 'user_type', @@ -106,6 +104,57 @@ const formSchema = z.object({ }); ``` +### Labels + +Set field labels using the `label` property in the field definition: + +```tsx +{ + id: 'username', + component: props => ( + + ), +} +``` + +#### Right Labels + +You can also add a label on the right side of the field using the `labelRight` property: + +```tsx +{ + id: 'username', + component: props => ( + + ), +} +``` + +### Descriptions + +Add descriptions to fields using the `description` property: + +```tsx +{ + id: 'username', + component: props => ( + + ), +} +``` + +or using the `describe` method in Zod: + +```ts +const formSchema = z.object({ + username: z.string().describe('This is the username for your application.'), +}); +``` + ### Default Values Set default values for fields using the `default` method: diff --git a/apps/docs/package.json b/apps/docs/package.json index 51861f904..7782acf03 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -60,6 +60,7 @@ "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.6", "typescript": "^5.9.2", + "use-intl": "^4.3.4", "zod": "^4.0.14" } } diff --git a/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/password_reset/page.tsx b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/password_reset/page.tsx new file mode 100644 index 000000000..8c46b3f13 --- /dev/null +++ b/apps/docs/src/app/[locale]/(main)/(plugins)/(vitnode-core)/login/password_reset/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from 'next/dist/types'; + +import { getTranslations } from 'next-intl/server'; + +import { PasswordResetView } from '@vitnode/core/views/auth/password-reset/password-reset-view'; + +export const generateMetadata = async ({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise => { + const { locale } = await params; + const t = await getTranslations({ + locale, + namespace: 'core.auth.password_reset', + }); + + return { + title: t('title'), + }; +}; + +export default function Page() { + return ; +} diff --git a/apps/docs/src/examples/auto-form.tsx b/apps/docs/src/examples/auto-form.tsx index 87ff240d9..149f2ad67 100644 --- a/apps/docs/src/examples/auto-form.tsx +++ b/apps/docs/src/examples/auto-form.tsx @@ -38,7 +38,7 @@ export default function AutoFormExample() { { id: 'email', component: props => ( - + ), }, { diff --git a/apps/docs/src/examples/input.tsx b/apps/docs/src/examples/input.tsx index c0fe93052..a74d4dd09 100644 --- a/apps/docs/src/examples/input.tsx +++ b/apps/docs/src/examples/input.tsx @@ -29,7 +29,6 @@ export default function InputExample() { ), diff --git a/apps/docs/src/locales/@vitnode/core/en.json b/apps/docs/src/locales/@vitnode/core/en.json index bec6c674c..6fe36226b 100644 --- a/apps/docs/src/locales/@vitnode/core/en.json +++ b/apps/docs/src/locales/@vitnode/core/en.json @@ -132,7 +132,8 @@ }, "password": { "label": "Password", - "required": "Password is required." + "required": "Password is required.", + "reset": "Forgot password?" }, "errors": { "access_denied": { @@ -141,6 +142,11 @@ } }, "submit": "Login" + }, + "password_reset": { + "title": "Reset Password", + "desc": "Please enter your email address below to receive a password reset link.", + "submit": "Send Reset Link" } } }, diff --git a/packages/create-vitnode-app/src/create/create-package-json.ts b/packages/create-vitnode-app/src/create/create-package-json.ts index 27027c2b2..8014129e1 100644 --- a/packages/create-vitnode-app/src/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/create/create-package-json.ts @@ -70,7 +70,7 @@ export const createPackageJSON = async ({ } : {}), turbo: '^2.5.5', - typescript: '^5.8.3', + typescript: '^5.9.2', zod: '^4.0.14', }, packageManager: `${packageManager}@${availablePackageManagers[packageManager]}`, @@ -127,6 +127,7 @@ export const createPackageJSON = async ({ 'next-intl': '^4.3.1', react: '^19.1', 'react-dom': '^19.1', + 'use-intl': '^4.3.4', zod: '^4.0.14', }, devDependencies: { @@ -155,7 +156,7 @@ export const createPackageJSON = async ({ 'react-email': '^4.2.7', 'tsc-alias': '^1.8.16', tsx: '^4.20.3', - typescript: '^5.8.3', + typescript: '^5.9.2', }, }; @@ -203,6 +204,7 @@ export const createPackageJSON = async ({ 'react-dom': '^19.1', 'react-hook-form': '^7.61.1', sonner: '^2.0.6', + 'use-intl': '^4.3.4', zod: '^4.0.14', }, devDependencies: { @@ -222,7 +224,7 @@ export const createPackageJSON = async ({ turbo: '^2.5.5', tailwindcss: '^4.1.11', 'tw-animate-css': '^1.3.6', - typescript: '^5.8.3', + typescript: '^5.9.2', }, packageManager: `${packageManager}@${availablePackageManagers[packageManager]}`, }; @@ -282,7 +284,7 @@ export const createPackageJSON = async ({ postcss: '^8.5.6', tailwindcss: '^4.1.11', 'tw-animate-css': '^1.3.6', - typescript: '^5.8.3', + typescript: '^5.9.2', zod: '^4.0.14', }, }; diff --git a/packages/vitnode/src/app/login/password_reset/page.tsx b/packages/vitnode/src/app/login/password_reset/page.tsx new file mode 100644 index 000000000..1999c4ccf --- /dev/null +++ b/packages/vitnode/src/app/login/password_reset/page.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from 'next/dist/types'; + +import { getTranslations } from 'next-intl/server'; + +import { PasswordResetView } from '@/views/auth/password-reset/password-reset-view'; + +export const generateMetadata = async ({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise => { + const { locale } = await params; + const t = await getTranslations({ + locale, + namespace: 'core.auth.password_reset', + }); + + return { + title: t('title'), + }; +}; + +export default function Page() { + return ; +} diff --git a/packages/vitnode/src/components/form/auto-form.tsx b/packages/vitnode/src/components/form/auto-form.tsx index 963e4c677..b7a7a227c 100644 --- a/packages/vitnode/src/components/form/auto-form.tsx +++ b/packages/vitnode/src/components/form/auto-form.tsx @@ -28,6 +28,7 @@ export interface ItemAutoFormComponentProps { description?: React.ReactNode; field: ControllerRenderProps; label?: React.ReactNode; + labelRight?: React.ReactNode; otherProps: { enum?: string[]; isOptional?: boolean; @@ -74,6 +75,7 @@ export function AutoForm< captcha, fields, submitButtonProps, + children, ...props }: Omit, 'onSubmit'> & { captcha?: z.infer['captcha']; @@ -185,7 +187,10 @@ export function AutoForm< typeof params.pattern === 'string' ? params.pattern : undefined, - type: params.type === 'string' ? params.type : undefined, + type: + typeof params.type === 'string' + ? params.type + : undefined, }, })} @@ -195,6 +200,8 @@ export function AutoForm< ); })} + {children} + {captcha &&
} {setIsDirty ? ( diff --git a/packages/vitnode/src/components/form/common/label.tsx b/packages/vitnode/src/components/form/common/label.tsx index 49863a893..01257defd 100644 --- a/packages/vitnode/src/components/form/common/label.tsx +++ b/packages/vitnode/src/components/form/common/label.tsx @@ -1,8 +1,26 @@ import { FormLabel } from '@/components/ui/form'; +import { cn } from '@/lib/utils'; export const AutoFormLabel = ({ children, + labelRight, + className, ...props -}: React.ComponentProps) => { - return {children}; +}: React.ComponentProps & { + labelRight?: React.ReactNode; +}) => { + return ( + + {children} + {labelRight && {labelRight}} + + ); }; diff --git a/packages/vitnode/src/components/form/fields/checkbox.tsx b/packages/vitnode/src/components/form/fields/checkbox.tsx index 70f323d8b..d6d01ab63 100644 --- a/packages/vitnode/src/components/form/fields/checkbox.tsx +++ b/packages/vitnode/src/components/form/fields/checkbox.tsx @@ -7,6 +7,7 @@ import { AutoFormLabel } from '../common/label'; export const AutoFormCheckbox = ({ label, + labelRight, description, otherProps: { isOptional }, field, @@ -30,7 +31,9 @@ export const AutoFormCheckbox = ({ {!!(label ?? description) && (
{label && ( - {label} + + {label} + )} {description && {description}} diff --git a/packages/vitnode/src/components/form/fields/combobox-async.tsx b/packages/vitnode/src/components/form/fields/combobox-async.tsx index 7bf66e800..3df0449cf 100644 --- a/packages/vitnode/src/components/form/fields/combobox-async.tsx +++ b/packages/vitnode/src/components/form/fields/combobox-async.tsx @@ -33,6 +33,7 @@ export const AutoFormComboboxAsync = ({ description, placeholder, className, + labelRight, id, otherProps: { isOptional }, searchPlaceholder, @@ -70,7 +71,11 @@ export const AutoFormComboboxAsync = ({ return ( - {label && {label}} + {label && ( + + {label} + + )} diff --git a/packages/vitnode/src/components/form/fields/combobox.tsx b/packages/vitnode/src/components/form/fields/combobox.tsx index d4ebfce73..d44e6e5b7 100644 --- a/packages/vitnode/src/components/form/fields/combobox.tsx +++ b/packages/vitnode/src/components/form/fields/combobox.tsx @@ -32,6 +32,7 @@ export const AutoFormCombobox = ({ className, otherProps: { enum: enumValues = [], isOptional }, labels = [], + labelRight, searchPlaceholder, ...props }: ItemAutoFormComponentProps & @@ -53,7 +54,11 @@ export const AutoFormCombobox = ({ return ( - {label && {label}} + {label && ( + + {label} + + )} diff --git a/packages/vitnode/src/components/form/fields/input.tsx b/packages/vitnode/src/components/form/fields/input.tsx index 7ad2b3f85..3e9beed8f 100644 --- a/packages/vitnode/src/components/form/fields/input.tsx +++ b/packages/vitnode/src/components/form/fields/input.tsx @@ -7,6 +7,7 @@ import { AutoFormLabel } from '../common/label'; export const AutoFormInput = ({ label, + labelRight, description, otherProps: { isOptional, maxLength, minLength, pattern, type }, field, @@ -15,7 +16,11 @@ export const AutoFormInput = ({ Omit, 'value'>) => { return ( - {label && {label}} + {label && ( + + {label} + + )} - {label && {label}} + {label && ( + + {label} + + )} - {label && {label}} + {label && ( + + {label} + + )}