Skip to content

Commit

Permalink
implement settings update functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Ayon95 committed Mar 23, 2024
1 parent 499f4d2 commit 397baee
Show file tree
Hide file tree
Showing 16 changed files with 513 additions and 2 deletions.
129 changes: 129 additions & 0 deletions src/features/settings/UpdateSettingsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

import { SettingsUpdateDTO } from '@/types/settings';
import Form from '@/ui/form/Form';
import FormControl from '@/ui/form/FormControl';
import { useUpdateSettings } from './hooks/useUpdateSettings';
import { Button } from '@/ui/button/Button';
import SpinnerMini from '@/ui/spinner/SpinnerMini';
import { getModifiedFormFieldValues } from '@/utils/helpers';
import { Tables } from '@/types/database';

interface UpdateSettingsFormProps {
settings: Tables<'settings'>;
}

const formSchema = z.object({
min_booking_length: z.coerce
.number()
.int({ message: 'Min nights per booking must be a whole number' })
.positive({ message: 'Min nights per booking must be greater than 0' }),
max_booking_length: z.coerce
.number()
.int({ message: 'Max nights per booking must be a whole number' })
.positive({ message: 'Max nights per booking must be greater than 0' }),
max_guests_per_booking: z.coerce
.number()
.int({ message: 'Max guest per booking must be a whole number' })
.positive({ message: 'Max guests per booking must be greater than 0' }),
breakfast_price: z.coerce
.number()
.positive({ message: 'Breakfast price must be greater than 0' }),
});

type FormData = z.infer<typeof formSchema>;

function UpdateSettingsForm({ settings }: UpdateSettingsFormProps) {
const { min_booking_length, max_booking_length, max_guests_per_booking, breakfast_price } =
settings;

const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
min_booking_length,
max_booking_length,
max_guests_per_booking,
breakfast_price,
},
});

const formErrors = form.formState.errors;
const dirtyFields = form.formState.dirtyFields;

const updateSettingsMutation = useUpdateSettings();

function handleSubmit(formData: FormData) {
const updatedData: SettingsUpdateDTO = getModifiedFormFieldValues(dirtyFields, formData);
updateSettingsMutation.mutate({ settingsId: settings.id, updatedData });
}

return (
<Form
onSubmit={form.handleSubmit(handleSubmit)}
aria-labelledby="updateSettingsFormLabel"
noValidate
>
<span className="sr-only" id="updateSettingsFormLabel">
Update hotel settings
</span>
<div className="multi-col-input-container">
<FormControl
labelInfo={{ label: 'Min nights per booking', inputId: 'minBookingLength' }}
error={formErrors.min_booking_length?.message}
>
<input
{...form.register('min_booking_length')}
type="number"
id="minBookingLength"
aria-invalid={formErrors.min_booking_length ? 'true' : 'false'}
disabled={updateSettingsMutation.isLoading}
/>
</FormControl>
<FormControl
labelInfo={{ label: 'Max nights per booking', inputId: 'maxBookingLength' }}
error={formErrors.max_booking_length?.message}
>
<input
{...form.register('max_booking_length')}
type="number"
id="maxBookingLength"
aria-invalid={formErrors.max_booking_length ? 'true' : 'false'}
disabled={updateSettingsMutation.isLoading}
/>
</FormControl>
</div>
<FormControl
labelInfo={{ label: 'Max guests per booking', inputId: 'maxGuestsPerBooking' }}
error={formErrors.max_guests_per_booking?.message}
>
<input
{...form.register('max_guests_per_booking')}
type="number"
id="maxGuestsPerBooking"
aria-invalid={formErrors.max_guests_per_booking ? 'true' : 'false'}
disabled={updateSettingsMutation.isLoading}
/>
</FormControl>
<FormControl
labelInfo={{ label: 'Breakfast price', inputId: 'breakfastPrice' }}
error={formErrors.breakfast_price?.message}
>
<input
{...form.register('breakfast_price')}
type="number"
id="breakfastPrice"
aria-invalid={formErrors.breakfast_price ? 'true' : 'false'}
disabled={updateSettingsMutation.isLoading}
/>
</FormControl>
<Button $size="large" disabled={updateSettingsMutation.isLoading}>
{updateSettingsMutation.isLoading ? <SpinnerMini /> : 'Update Settings'}
</Button>
{updateSettingsMutation.isError && <p>{updateSettingsMutation.error.message}</p>}
</Form>
);
}

export default UpdateSettingsForm;
94 changes: 94 additions & 0 deletions src/features/settings/__tests__/UpdateSettingsForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { renderWithQueryClient } from '@/test/utils';
import UpdateSettingsForm from '../UpdateSettingsForm';
import { settings } from '@/test/fixtures/settings';

const formTitleRegex = /update hotel settings/i;
const submitButtonRegex = /update settings/i;

function setup() {
renderWithQueryClient(<UpdateSettingsForm settings={settings} />);
}

describe('UpdateSettingsForm', () => {
it('should render a form element', () => {
setup();
const updateSettingsForm = screen.getByRole('form', { name: formTitleRegex });
expect(updateSettingsForm).toBeInTheDocument();
});

it('should have all the necessary form fields and submit button', () => {
setup();

const minNightsPerBookingInput = screen.getByLabelText(/min nights per booking/i);
const maxNightsPerBookingInput = screen.getByLabelText(/max nights per booking/i);
const maxGuestsPerBookingInput = screen.getByLabelText(/max guests per booking/i);
const breakfastPriceInput = screen.getByLabelText(/breakfast price/i);
const submitButton = screen.getByRole('button', { name: submitButtonRegex });

[
minNightsPerBookingInput,
maxNightsPerBookingInput,
maxGuestsPerBookingInput,
breakfastPriceInput,
submitButton,
].forEach(element => {
expect(element).toBeInTheDocument();
});
});

it('should have form fields pre-filled with settings data', () => {
setup();

const minNightsPerBookingInput = screen.getByDisplayValue(settings.min_booking_length);
const maxNightsPerBookingInput = screen.getByDisplayValue(settings.max_booking_length);
const maxGuestsPerBookingInput = screen.getByDisplayValue(settings.max_guests_per_booking);
const breakfastPriceInput = screen.getByDisplayValue(settings.breakfast_price);

[
minNightsPerBookingInput,
maxNightsPerBookingInput,
maxGuestsPerBookingInput,
breakfastPriceInput,
].forEach(element => {
expect(element).toBeInTheDocument();
});
});

it('should show error messages if input values are not provided', async () => {
setup();
const user = userEvent.setup();

await user.clear(screen.getByLabelText(/min nights per booking/i));
await user.clear(screen.getByLabelText(/max nights per booking/i));
await user.clear(screen.getByLabelText(/max guests per booking/i));
await user.clear(screen.getByLabelText(/breakfast price/i));

await user.click(screen.getByRole('button', { name: submitButtonRegex }));

const minNightsPerBookingError = screen.getByText(
/min nights per booking must be greater than 0/i
);

const maxNightsPerBookingError = screen.getByText(
/max nights per booking must be greater than 0/i
);

const maxGuestsPerBookingError = screen.getByText(
/max guests per booking must be greater than 0/i
);

const breakfastPriceError = screen.getByText(/breakfast price must be greater than 0/i);

[
minNightsPerBookingError,
maxNightsPerBookingError,
maxGuestsPerBookingError,
breakfastPriceError,
].forEach(element => {
expect(element).toBeInTheDocument();
});
});
});
12 changes: 12 additions & 0 deletions src/features/settings/hooks/useSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getSettings } from '@/services/apiSettings';
import { SETTINGS_QUERY_KEY } from '@/utils/constants';
import { useQuery } from '@tanstack/react-query';

export function useSettings(userId: string | null | undefined) {
return useQuery({
queryKey: [SETTINGS_QUERY_KEY, userId],
queryFn: () => getSettings(userId),
staleTime: Infinity,
cacheTime: Infinity,
});
}
18 changes: 18 additions & 0 deletions src/features/settings/hooks/useUpdateSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';

import { updateSettings } from '@/services/apiSettings';
import { SETTINGS_QUERY_KEY } from '@/utils/constants';

export function useUpdateSettings() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: updateSettings,
onSuccess: () => {
toast.success('Settings updated successfully!');
queryClient.invalidateQueries({ queryKey: [SETTINGS_QUERY_KEY] });
},
onError: (error: Error) => toast.error(error.message),
});
}
19 changes: 18 additions & 1 deletion src/pages/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { useUser } from '@/features/authentication/hooks/useUser';
import UpdateSettingsForm from '@/features/settings/UpdateSettingsForm';
import { useSettings } from '@/features/settings/hooks/useSettings';
import Heading from '@/ui/Heading';
import Spinner from '@/ui/spinner/Spinner';

function Settings() {
return <Heading as="h1">Update hotel settings</Heading>;
const { data: user } = useUser();
const { data: settings, isLoading } = useSettings(user?.id);
return (
<>
<Heading as="h1">Update hotel settings</Heading>
{isLoading ? (
<Spinner />
) : settings ? (
<UpdateSettingsForm settings={settings} />
) : (
<p className="mt-1">Settings could not be loaded.</p>
)}
</>
);
}

export default Settings;
Loading

0 comments on commit 397baee

Please sign in to comment.