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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The production technology stack includes:
- Font Awesome - iconography
- Google Fonts - typography
- React i18next - internationalization
- Yup - schema based validation
- Zod - schema based validation
- Lodash - utility functions
- DayJS - date and time utility functions
- TanStack Table - advanced tables and datagrids
Expand Down Expand Up @@ -268,7 +268,7 @@ This project uses GitHub Actions to perform DevOps automation activities such as
- [TanStack][tanstack]
- [Axios][axios]
- [React Hook Form][reacthookform]
- [Yup][yup]
- [Zod][zod]
- [Tailwind CSS][tailwind]
- [Class Variance Authority][cva]
- [Font Awesome][fontawesome]
Expand All @@ -287,7 +287,6 @@ This project uses GitHub Actions to perform DevOps automation activities such as
[vite]: https://vitejs.dev/ 'Vite'
[axios]: https://axios-http.com/ 'Axios'
[reacthookform]: https://www.react-hook-form.com/ 'React Hook Form'
[yup]: https://github.com/jquense/yup 'Yup'
[tailwind]: https://tailwindcss.com/ 'Tailwind CSS'
[cva]: https://cva.style/ 'Class Variance Authority'
[fontawesome]: https://fontawesome.com/ 'Font Awesome'
Expand All @@ -298,3 +297,4 @@ This project uses GitHub Actions to perform DevOps automation activities such as
[reactspring]: https://www.react-spring.dev/ 'React Spring'
[storybook]: https://storybook.js.org/ 'Storybook'
[recharts]: https://recharts.org/ 'Recharts'
[zod]: https://zod.dev/ 'Zod'
204 changes: 92 additions & 112 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
"@fortawesome/react-fontawesome": "0.2.2",
"@hookform/resolvers": "4.1.3",
"@react-spring/web": "9.7.5",
"@tailwindcss/vite": "4.0.17",
"@tanstack/react-query": "5.71.1",
"@tanstack/react-query-devtools": "5.71.1",
"@tailwindcss/vite": "4.1.1",
"@tanstack/react-query": "5.71.3",
"@tanstack/react-query-devtools": "5.71.3",
"@tanstack/react-table": "8.21.2",
"axios": "1.8.4",
"class-variance-authority": "0.7.1",
Expand All @@ -50,10 +50,10 @@
"react-i18next": "15.4.1",
"react-router-dom": "7.4.1",
"recharts": "2.15.1",
"tailwind-merge": "3.0.2",
"tailwindcss": "4.0.17",
"tailwind-merge": "3.1.0",
"tailwindcss": "4.1.1",
"uuid": "11.1.0",
"yup": "1.6.1"
"zod": "3.24.2"
},
"devDependencies": {
"@chromatic-com/storybook": "3.2.6",
Expand Down
12 changes: 6 additions & 6 deletions src/common/components/Form/__stories__/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useForm } from 'react-hook-form';
import { InferType, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { default as MyInput } from '../Input';
import { InputProps } from '../Input';

const formSchema = object({
color: string().required('Required'),
const formSchema = z.object({
color: z.string().min(1, { message: 'Required' }),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for the `Input` component. Provides the RHF form `control`
Expand All @@ -22,7 +22,7 @@ const Input = (props: Omit<InputProps<FormValues>, 'control'>) => {
color: '',
},
mode: 'all',
resolver: yupResolver(formSchema),
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down
16 changes: 7 additions & 9 deletions src/common/components/Form/__stories__/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useForm } from 'react-hook-form';
import { InferType, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { default as MySelect } from '../Select';
import { SelectProps } from '../Select';

const formSchema = object({
color: string().required('Required. ').oneOf(['blue', 'red'], 'Must be blue or red. '),
const formSchema = z.object({
color: z.enum(['blue', 'red'], { message: 'Must be blue or red.' }),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for the `Select` component. Provides the RHF form `control`
* to the `Select` component.
*/
const Select = (props: Omit<SelectProps<FormValues>, 'control'>) => {
const form = useForm({
defaultValues: {
color: '',
},
defaultValues: {},
mode: 'all',
resolver: yupResolver(formSchema),
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down
12 changes: 6 additions & 6 deletions src/common/components/Form/__stories__/Toggle.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useForm } from 'react-hook-form';
import { boolean, InferType, object } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { default as MyToggle } from '../Toggle';
import { ToggleProps } from '../Toggle';

const formSchema = object({
isEnabledNotifications: boolean(),
const formSchema = z.object({
isEnabledNotifications: z.boolean(),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for the `Toggle` component. Provides the RHF form `control`
Expand All @@ -22,7 +22,7 @@ const Toggle = (props: Omit<ToggleProps<FormValues>, 'control'>) => {
isEnabledNotifications: false,
},
mode: 'all',
resolver: yupResolver(formSchema),
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down
12 changes: 6 additions & 6 deletions src/common/components/Form/__tests__/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { describe, expect, it } from 'vitest';
import userEvent from '@testing-library/user-event';
import { InferType, object, string } from 'yup';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { render, screen } from 'test/test-utils';

import Input, { InputProps } from '../Input';

const formSchema = object({
color: string().required('Required'),
const formSchema = z.object({
color: z.string().min(1, { message: 'Required' }),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for testing the `Input` component which requires some
Expand All @@ -21,7 +21,7 @@ type FormValues = InferType<typeof formSchema>;
const InputWrapper = (props: Omit<InputProps<FormValues>, 'control'>) => {
const form = useForm<FormValues>({
defaultValues: { color: '' },
resolver: yupResolver(formSchema),
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down
19 changes: 8 additions & 11 deletions src/common/components/Form/__tests__/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import { useForm } from 'react-hook-form';
import { InferType, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { render, screen, waitFor } from 'test/test-utils';

import Select, { SelectProps } from '../Select';

const formSchema = object({
color: string().oneOf(['blue'], 'Must select a value in the list.'),
const formSchema = z.object({
color: z.string().refine((val) => ['blue'].includes(val), { message: 'Must select blue.' }),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for testing the `Select` component which requires some
* react-hook-form objects passed as props.
*/
const SelectWrapper = (props: Omit<SelectProps<FormValues>, 'control'>) => {
const form = useForm<FormValues>({
defaultValues: { color: '' },
resolver: yupResolver(formSchema),
defaultValues: {},
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down Expand Up @@ -257,8 +256,6 @@ describe('Select', () => {
await screen.findByTestId('select-error');

// ASSERT
expect(screen.getByTestId('select-error')).toHaveTextContent(
/must select a value in the list/i,
);
expect(screen.getByTestId('select-error')).toHaveTextContent(/must select blue/i);
});
});
13 changes: 6 additions & 7 deletions src/common/components/Form/__tests__/Textarea.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { describe, expect, it } from 'vitest';
import userEvent from '@testing-library/user-event';
import { InferType, object, string } from 'yup';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { render, screen } from 'test/test-utils';

import Textarea, { TextareaProps } from '../Textarea';

const formSchema = object({
myField: string().required('Required'),
const formSchema = z.object({
myField: z.string().min(1, { message: 'Required' }),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for testing the `Textarea` component which requires some
Expand All @@ -21,7 +20,7 @@ type FormValues = InferType<typeof formSchema>;
const TextareaWrapper = (props: Omit<TextareaProps<FormValues>, 'control'>) => {
const form = useForm<FormValues>({
defaultValues: { myField: '' },
resolver: yupResolver(formSchema),
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down
17 changes: 9 additions & 8 deletions src/common/components/Form/__tests__/Toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import userEvent from '@testing-library/user-event';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { boolean, InferType, object } from 'yup';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

import { describe, expect, it } from 'vitest';
import { render, screen } from 'test/test-utils';

import Toggle, { ToggleProps } from '../Toggle';

const formSchema = object({
myField: boolean().oneOf([false]),
const formSchema = z.object({
myField: z.boolean().refine((val) => val === false, {
message: 'Must be false.',
}),
});

type FormValues = InferType<typeof formSchema>;
type FormValues = z.infer<typeof formSchema>;

/**
* A wrapper for testing the `Toggle` component which requires some
Expand All @@ -21,7 +22,7 @@ type FormValues = InferType<typeof formSchema>;
const ToggleWrapper = (props: Omit<ToggleProps<FormValues>, 'control'>) => {
const form = useForm<FormValues>({
defaultValues: { myField: false },
resolver: yupResolver(formSchema),
resolver: zodResolver(formSchema),
});

const onSubmit = () => {};
Expand Down Expand Up @@ -78,6 +79,6 @@ describe('Toggle', () => {
await screen.findByTestId('toggle-error');

// ASSERT
expect(screen.getByTestId('toggle-error')).toHaveTextContent(/must be one of the following/i);
expect(screen.getByTestId('toggle-error')).toHaveTextContent(/must be false/i);
});
});
35 changes: 17 additions & 18 deletions src/common/providers/ConfigProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { PropsWithChildren, useEffect, useState } from 'react';
import { number, object, ObjectSchema, string, ValidationError } from 'yup';
import { z, ZodError } from 'zod';

import { Config, ConfigContext } from './ConfigContext';

/**
* The configuration validation schema.
* @see {@link https://github.com/jquense/yup | Yup}
*/
const configSchema: ObjectSchema<Config> = object({
VITE_BASE_URL_API: string().url().required(),
VITE_BUILD_DATE: string().default('1970-01-01'),
VITE_BUILD_TIME: string().default('00:00:00'),
VITE_BUILD_TS: string().default('1970-01-01T00:00:00+0000'),
VITE_BUILD_COMMIT_SHA: string().default('local'),
VITE_BUILD_ENV_CODE: string().default('local'),
VITE_BUILD_WORKFLOW_NAME: string().default('local'),
VITE_BUILD_WORKFLOW_RUN_NUMBER: number().default(1),
VITE_BUILD_WORKFLOW_RUN_ATTEMPT: number().default(1),
VITE_TOAST_AUTO_DISMISS_MILLIS: number().default(5000),
const configSchema = z.object({
VITE_BASE_URL_API: z.string().url(),
VITE_BUILD_DATE: z.string().default('1970-01-01'),
VITE_BUILD_TIME: z.string().default('00:00:00'),
VITE_BUILD_TS: z.string().default('1970-01-01T00:00:00+0000'),
VITE_BUILD_COMMIT_SHA: z.string().default('local'),
VITE_BUILD_ENV_CODE: z.string().default('local'),
VITE_BUILD_WORKFLOW_NAME: z.string().default('local'),
VITE_BUILD_WORKFLOW_RUN_NUMBER: z.coerce.number().default(1),
VITE_BUILD_WORKFLOW_RUN_ATTEMPT: z.coerce.number().default(1),
VITE_TOAST_AUTO_DISMISS_MILLIS: z.coerce.number().default(5000),
});

/**
Expand All @@ -35,14 +34,14 @@ const ConfigContextProvider = ({ children }: PropsWithChildren): JSX.Element =>

useEffect(() => {
try {
const validatedConfig = configSchema.validateSync(import.meta.env, {
abortEarly: false,
stripUnknown: true,
});
const validatedConfig = configSchema.parse(import.meta.env);
setConfig(validatedConfig);
setIsReady(true);
} catch (err) {
if (err instanceof ValidationError) throw new Error(`${err}::${err.errors}`);
if (err instanceof ZodError) {
const errors = err.errors.map((e) => `${e.path.join('.')}::${e.message}`);
throw new Error(`Configuration error: ${errors.join(', ')}`);
}
if (err instanceof Error) throw new Error(`Configuration error: ${err.message}`);
throw err;
}
Expand Down
Loading