Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions public/assets/icons/general/CheckCircleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { FC, SVGProps } from 'react';

type IconColor = 'error' | 'success' | 'disabled' | 'outline' | 'primaryOutline';

const colorMap: Record<IconColor, string> = {
error: '#F04438',
success: '#067647',
disabled: '#D0D5DD',
outline: '#344054',
primaryOutline: '#1570ef',
};

interface CheckCircleIconProps extends SVGProps<SVGSVGElement> {
width?: number;
height?: number;
color?: IconColor;
}

/**
* A reusable SVG icon component for rendering an icon.
*
* @param {number} [width=24] - The width of the icon in pixels. Optional.
* @param {number} [height=24] - The height of the icon in pixels. Optional.
* @param {IconColor} [color='disabled'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
* @param {SVGProps<SVGSVGElement>} props - Additional SVG props such as `className`, `style`, or custom attributes.
*
* @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
*/

const CheckCircleIcon: FC<CheckCircleIconProps> = ({
width = 24,
height = 24,
color = 'disabled',
...props
}) => {
const isOutline = color === 'outline';
const isPrimaryOutline = color === 'primaryOutline';
const fillColor = isOutline || isPrimaryOutline ? 'none' : colorMap[color];
const strokeColor = isOutline
? colorMap['outline']
: isPrimaryOutline
? colorMap['primaryOutline']
: 'none';

return (
<svg
width={width}
height={height}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
aria-label='Check Circle Icon'
role='img'
{...props}
>
<circle
cx='12'
cy='12'
r='11'
fill={fillColor}
stroke={strokeColor}
strokeWidth={isOutline || isPrimaryOutline ? 2 : 0}
/>

<path
d='M8 12L10.5 14.5L16 9'
stroke={
isOutline ? colorMap['outline'] : isPrimaryOutline ? colorMap['primaryOutline'] : 'white'
}
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

export default CheckCircleIcon;
69 changes: 69 additions & 0 deletions public/assets/icons/general/XCircleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { FC, SVGProps } from 'react';

type IconColor = 'error' | 'success' | 'disabled' | 'outline';

const colorMap: Record<IconColor, string> = {
error: '#F04438',
success: '#067647',
disabled: '#D0D5DD',
outline: '#344054',
};

interface XCircleIconProps extends SVGProps<SVGSVGElement> {
width?: number;
height?: number;
color?: IconColor;
}

/**
* A reusable SVG icon component for rendering an icon.
*
* @param {number} [width=24] - The width of the icon in pixels. Optional.
* @param {number} [height=24] - The height of the icon in pixels. Optional.
* @param {IconColor} [color='disabled'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
* @param {SVGProps<SVGSVGElement>} props - Additional SVG props such as `className`, `style`, or custom attributes.
*
* @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
*/

const XCircleIcon: FC<XCircleIconProps> = ({
width = 24,
height = 24,
color = 'disabled',
...props
}) => {
const isOutline = color === 'outline';
const fillColor = isOutline ? 'none' : colorMap[color];
const strokeColor = isOutline ? colorMap['outline'] : colorMap[color];

return (
<svg
width={width}
height={height}
viewBox='0 0 24 24'
fill={fillColor}
xmlns='http://www.w3.org/2000/svg'
aria-label='X Circle Icon'
role='img'
{...props}
>
<circle
cx='12'
cy='12'
r='11'
fill={fillColor}
stroke={strokeColor}
strokeWidth={isOutline ? 2 : 0}
/>
<path
d='M15 9L9 15M9 9L15 15'
stroke={isOutline ? colorMap['outline'] : 'white'}
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

export default XCircleIcon;
2 changes: 2 additions & 0 deletions public/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { default as SearchIcon } from './access/SearchIcon';

export { default as EmptyStateIcon } from './general/EmptyStateIcon';
export { default as SpinnerIcon } from './general/SpinnerIcon';
export { default as CheckCircleIcon } from './general/CheckCircleIcon';
export { default as XCircleIcon } from './general/XCircleIcon';
83 changes: 83 additions & 0 deletions src/app/(client)/(auth)/components/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import React from 'react';

import NextLink from 'next/link';
import { useRouter } from 'next/navigation';

import { Button, FormInput } from '@components';

import { useForgotPasswordMutation } from '@/app/(client)/hooks/data';
import { useForgotPasswordForm, useFormSubmission } from '@/app/(client)/hooks/forms';
import { Form } from '@/shadcn-ui';

export default function ForgotPasswordForm() {
const router = useRouter();
const forgotPasswordMutation = useForgotPasswordMutation();
const form = useForgotPasswordForm();

const {
getValues,
formState: { isValid },
} = form;

const { loading, handleSubmit, toast } = useFormSubmission({
mutation: forgotPasswordMutation,
getVariables: () => getValues(),
validate: () => isValid,
onSuccess: async (res) => {
// Dev-only: jump straight to reset with token
if (res?.token) {
router.replace(`/password/reset?token=${encodeURIComponent(res.token)}`);
} else {
// Fallback: a dev page that explains what's happening
router.replace('/password/reset?dev=1');
}
},
onError: (err) => {
const message =
err instanceof Error && err.message
? err.message
: 'Could not process password reset request.';
toast.showToast({ message, variant: 'error' });
},
skipDefaultToast: true,
});

return (
<>
<div className='flex flex-col items-center gap-4'>
<h1 className='h1'>Forgot password?</h1>
<p className='body2'>Enter your account email to continue.</p>
</div>
<Form {...form}>
<form
onSubmit={handleSubmit}
className='min-w-[25em]'
>
<div className='mb-10 flex flex-col gap-5'>
<FormInput
control={form.control}
name='email'
label='Email'
type='email'
placeholder='[email protected]'
/>
</div>
<div>
<Button
type='submit'
fullWidth
disabled={!isValid || loading}
loading={loading}
loadingText='Processing...'
>
Reset password
</Button>
</div>
</form>
</Form>
<NextLink href='/login'>← Back to login</NextLink>
</>
);
}
4 changes: 2 additions & 2 deletions src/app/(client)/(auth)/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function LoginForm() {
router.replace(nextUrl);
},
onError: (err) => {
const message = err.message ?? 'Unable to log in!';
const message = err instanceof Error && err.message ? err.message : 'Unable to log in!';
toast.showToast({ message, variant: 'error' });
},
skipDefaultToast: true,
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function LoginForm() {
<Button
type='submit'
fullWidth
disabled={!isValid}
disabled={!isValid || loading}
loading={loading}
loadingText='Logging in...'
>
Expand Down
18 changes: 14 additions & 4 deletions src/app/(client)/(auth)/components/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react';

import { useRouter } from 'next/navigation';

import { Button, FormInput } from '@components';
import { Button, FormInput, PasswordValidation } from '@components';

import { useLoginMutation, useRegisterMutation } from '@/app/(client)/hooks/data';
import { useFormSubmission, useRegisterForm } from '@/app/(client)/hooks/forms';
Expand All @@ -19,6 +19,8 @@ export default function RegisterForm() {
const {
getValues,
formState: { isValid },
watchPassword,
isPasswordTouched,
} = form;

const { loading, handleSubmit, toast } = useFormSubmission({
Expand All @@ -39,7 +41,7 @@ export default function RegisterForm() {
router.replace('/dashboard');
} catch (err) {
const message =
err instanceof Error
err instanceof Error && err.message
? err.message
: 'Account created, but we couldn’t sign you in automatically. Please log in.';
toast.showToast({ message, variant: 'error' });
Expand All @@ -49,7 +51,8 @@ export default function RegisterForm() {
}
},
onError: (err) => {
const message = err.message ?? 'Unable to create account!';
const message =
err instanceof Error && err.message ? err.message : 'Unable to create account!';
toast.showToast({ message, variant: 'error' });
},
skipDefaultToast: true,
Expand Down Expand Up @@ -95,11 +98,18 @@ export default function RegisterForm() {
placeholder='Confirm your password'
/>
</div>

{/* Real-time password strength feedback */}
<PasswordValidation
passwordValue={watchPassword}
isBlur={isPasswordTouched}
/>

<div>
<Button
type='submit'
fullWidth
disabled={!isValid}
disabled={!isValid || loading}
loading={loading}
loadingText='Creating account...'
>
Expand Down
Loading