[frontend]: add reset password

This commit is contained in:
Sergey Krylov 2025-08-03 08:46:07 +03:00
parent 857374be4e
commit 75930d105e
9 changed files with 373 additions and 0 deletions

View File

@ -0,0 +1,20 @@
import { NewPasswordForm } from '@/components/features/auth/forms/NewPasswordForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import React from 'react';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.newPassword');
return {
title: t('heading')
}
}
const NewPasswordPage = () => {
return (
<NewPasswordForm/>
);
};
export default NewPasswordPage;

View File

@ -0,0 +1,20 @@
import { ResetPasswordForm } from '@/components/features/auth/forms/ResetPasswordForm';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import React from 'react';
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations('auth.resetPassword');
return {
title: t('heading')
}
}
const ResetPasswordPage = () => {
return (
<ResetPasswordForm/>
);
};
export default ResetPasswordPage;

View File

@ -0,0 +1,119 @@
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslations } from 'next-intl'
import { useParams, useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { Button } from '@/components/ui/common/Button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel
} from '@/components/ui/common/Form'
import { Input } from '@/components/ui/common/Input'
import { useNewPasswordMutation } from '@/graphql/generated/output'
import AuthWrapper from '../AuthWrapper'
import { newPasswordSchema, TypeNewPasswordSchema } from '@/schemas/auth/new-password.schema'
export function NewPasswordForm() {
const t = useTranslations('auth.newPassword')
const router = useRouter()
const params = useParams<{ token: string }>()
const form = useForm<TypeNewPasswordSchema>({
resolver: zodResolver(newPasswordSchema),
defaultValues: {
password: '',
passwordRepeat: ''
}
})
const [newPassword, { loading: isLoadingNew }] = useNewPasswordMutation({
onCompleted(data) {
toast.success(t('successMessage'))
router.push('/account/login')
},
onError() {
toast.error(t('errorMessage'))
}
})
const { isValid } = form.formState
function onSubmit(data: TypeNewPasswordSchema) {
newPassword({ variables: { data: { ...data, token: params.token } } })
}
return (
<AuthWrapper
heading={t('heading')}
backButtonLabel={t('backButtonLabel')}
backButtonHref='/account/login'
>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='grid gap-y-3'
>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
placeholder='********'
type='password'
disabled={isLoadingNew}
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='passwordRepeat'
render={({ field }) => (
<FormItem>
<FormLabel>
{t('passwordRepeatLabel')}
</FormLabel>
<FormControl>
<Input
placeholder='********'
type='password'
disabled={isLoadingNew}
{...field}
/>
</FormControl>
<FormDescription>
{t('passwordRepeatDescription')}
</FormDescription>
</FormItem>
)}
/>
<Button
className='mt-2 w-full'
disabled={!isValid || isLoadingNew}
>
{t('submitButton')}
</Button>
</form>
</Form>
</AuthWrapper>
)
}

View File

@ -0,0 +1,113 @@
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { CircleCheck } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Alert,
AlertDescription,
AlertTitle
} from '@/components/ui/common/Alert'
import { Button } from '@/components/ui/common/Button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel
} from '@/components/ui/common/Form'
import { Input } from '@/components/ui/common/Input'
import { useResetPasswordMutation } from '@/graphql/generated/output'
import {
type TypeResetPasswordSchema,
resetPasswordSchema
} from '@/schemas/auth/reset-password.schema'
import AuthWrapper from '../AuthWrapper'
export function ResetPasswordForm() {
const t = useTranslations('auth.resetPassword')
const [isSuccess, setIsSuccess] = useState(false)
const form = useForm<TypeResetPasswordSchema>({
resolver: zodResolver(resetPasswordSchema),
defaultValues: {
email: ''
}
})
const [resetPassword, { loading: isLoadingReset }] =
useResetPasswordMutation({
onCompleted() {
setIsSuccess(true)
},
onError() {
toast.error(t('errorMessage'))
}
})
const { isValid } = form.formState
function onSubmit(data: TypeResetPasswordSchema) {
void resetPassword({ variables: { data } })
}
return (
<AuthWrapper
heading={t('heading')}
backButtonLabel={t('backButtonLabel')}
backButtonHref='/account/login'
>
{isSuccess ? (
<Alert>
<CircleCheck className='size-4' />
<AlertTitle>{t('successAlertTitle')}</AlertTitle>
<AlertDescription>
{t('successAlertDescription')}
</AlertDescription>
</Alert>
) : (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='grid gap-y-3'
>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
placeholder='john.doe@example.com'
disabled={isLoadingReset}
{...field}
/>
</FormControl>
<FormDescription>
{t('emailDescription')}
</FormDescription>
</FormItem>
)}
/>
<Button
className='mt-2 w-full'
disabled={!isValid || isLoadingReset}
>
{t('submitButton')}
</Button>
</form>
</Form>
)}
</AuthWrapper>
)
}

View File

@ -605,6 +605,20 @@ export type LoginUserMutationVariables = Exact<{
export type LoginUserMutation = { __typename?: 'Mutation', loginUser: { __typename?: 'AuthModel', message?: string | null, user?: { __typename?: 'UserModel', id: string, name: string, email: string } | null } };
export type NewPasswordMutationVariables = Exact<{
data: NewPasswordInput;
}>;
export type NewPasswordMutation = { __typename?: 'Mutation', setNewPassword: boolean };
export type ResetPasswordMutationVariables = Exact<{
data: ResetPasswordInput;
}>;
export type ResetPasswordMutation = { __typename?: 'Mutation', resetPassword: boolean };
export type VerifyAccountMutationVariables = Exact<{
data: VerificationInput;
}>;
@ -693,6 +707,68 @@ export function useLoginUserMutation(baseOptions?: Apollo.MutationHookOptions<Lo
export type LoginUserMutationHookResult = ReturnType<typeof useLoginUserMutation>;
export type LoginUserMutationResult = Apollo.MutationResult<LoginUserMutation>;
export type LoginUserMutationOptions = Apollo.BaseMutationOptions<LoginUserMutation, LoginUserMutationVariables>;
export const NewPasswordDocument = gql`
mutation NewPassword($data: NewPasswordInput!) {
setNewPassword(data: $data)
}
`;
export type NewPasswordMutationFn = Apollo.MutationFunction<NewPasswordMutation, NewPasswordMutationVariables>;
/**
* __useNewPasswordMutation__
*
* To run a mutation, you first call `useNewPasswordMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useNewPasswordMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [newPasswordMutation, { data, loading, error }] = useNewPasswordMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useNewPasswordMutation(baseOptions?: Apollo.MutationHookOptions<NewPasswordMutation, NewPasswordMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<NewPasswordMutation, NewPasswordMutationVariables>(NewPasswordDocument, options);
}
export type NewPasswordMutationHookResult = ReturnType<typeof useNewPasswordMutation>;
export type NewPasswordMutationResult = Apollo.MutationResult<NewPasswordMutation>;
export type NewPasswordMutationOptions = Apollo.BaseMutationOptions<NewPasswordMutation, NewPasswordMutationVariables>;
export const ResetPasswordDocument = gql`
mutation ResetPassword($data: ResetPasswordInput!) {
resetPassword(data: $data)
}
`;
export type ResetPasswordMutationFn = Apollo.MutationFunction<ResetPasswordMutation, ResetPasswordMutationVariables>;
/**
* __useResetPasswordMutation__
*
* To run a mutation, you first call `useResetPasswordMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useResetPasswordMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [resetPasswordMutation, { data, loading, error }] = useResetPasswordMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useResetPasswordMutation(baseOptions?: Apollo.MutationHookOptions<ResetPasswordMutation, ResetPasswordMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ResetPasswordMutation, ResetPasswordMutationVariables>(ResetPasswordDocument, options);
}
export type ResetPasswordMutationHookResult = ReturnType<typeof useResetPasswordMutation>;
export type ResetPasswordMutationResult = Apollo.MutationResult<ResetPasswordMutation>;
export type ResetPasswordMutationOptions = Apollo.BaseMutationOptions<ResetPasswordMutation, ResetPasswordMutationVariables>;
export const VerifyAccountDocument = gql`
mutation VerifyAccount($data: VerificationInput!) {
verifyAccount(data: $data) {

View File

@ -0,0 +1,3 @@
mutation NewPassword($data: NewPasswordInput!) {
setNewPassword(data: $data)
}

View File

@ -0,0 +1,3 @@
mutation ResetPassword($data: ResetPasswordInput!) {
resetPassword(data: $data)
}

View File

@ -0,0 +1,12 @@
import { z } from 'zod'
export const newPasswordSchema = z
.object({
password: z.string().min(8),
passwordRepeat: z.string().min(8)
})
.refine(data => data.password === data.passwordRepeat, {
path: ['passwordRepeat']
})
export type TypeNewPasswordSchema = z.infer<typeof newPasswordSchema>

View File

@ -0,0 +1,7 @@
import { z } from 'zod'
export const resetPasswordSchema = z.object({
email: z.string().email()
})
export type TypeResetPasswordSchema = z.infer<typeof resetPasswordSchema>