refactor availability times form using react-hook-form (#824)

* use toast

* use `Button`

* make fn

* rewrite with react-hook-form

* add comment

* fix deps

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Alex Johansson 2021-09-30 22:37:52 +01:00 committed by GitHub
parent 60298f6eeb
commit 30163f0a78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 192 additions and 246 deletions

View File

@ -28,6 +28,7 @@
"dependencies": {
"@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4",
"@hookform/resolvers": "^2.8.1",
"@jitsu/sdk-js": "^2.2.4",
"@prisma/client": "^2.30.2",
"@radix-ui/react-avatar": "^0.1.0",
@ -73,6 +74,7 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"react-easy-crop": "^3.5.2",
"react-hook-form": "^7.16.1",
"react-hot-toast": "^2.1.0",
"react-intl": "^5.20.7",
"react-multi-email": "^0.5.3",

View File

@ -1,36 +1,57 @@
import { ClockIcon } from "@heroicons/react/outline";
import { useSession } from "next-auth/client";
import Link from "next/link";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import showToast from "@lib/notification";
import { trpc } from "@lib/trpc";
import { Dialog, DialogContent } from "@components/Dialog";
import Loader from "@components/Loader";
import Modal from "@components/Modal";
import Shell from "@components/Shell";
import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button";
function convertMinsToHrsMins(mins: number) {
const h = Math.floor(mins / 60);
const m = mins % 60;
const hours = h < 10 ? "0" + h : h;
const minutes = m < 10 ? "0" + m : m;
return `${hours}:${minutes}`;
}
export default function Availability() {
const queryMe = trpc.useQuery(["viewer.me"]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [session, loading] = useSession();
const router = useRouter();
const [showAddModal, setShowAddModal] = useState(false);
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [showChangeTimesModal, setShowChangeTimesModal] = useState(false);
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
const isHiddenRef = useRef<HTMLInputElement>();
const formModal = useToggleQuery("edit");
const startHoursRef = useRef<HTMLInputElement>();
const startMinsRef = useRef<HTMLInputElement>();
const endHoursRef = useRef<HTMLInputElement>();
const endMinsRef = useRef<HTMLInputElement>();
const bufferHoursRef = useRef<HTMLInputElement>();
const bufferMinsRef = useRef<HTMLInputElement>();
const formMethods = useForm<{
startHours: string;
startMins: string;
endHours: string;
endMins: string;
bufferHours: string;
bufferMins: string;
}>({});
const router = useRouter();
useEffect(() => {
/**
* This hook populates the form with new values as soon as the user is loaded or changes
*/
const user = queryMe.data;
if (formMethods.formState.isDirty || !user) {
return;
}
formMethods.reset({
startHours: convertMinsToHrsMins(user.startTime).split(":")[0],
startMins: convertMinsToHrsMins(user.startTime).split(":")[1],
endHours: convertMinsToHrsMins(user.endTime).split(":")[0],
endMins: convertMinsToHrsMins(user.endTime).split(":")[1],
bufferHours: convertMinsToHrsMins(user.bufferTime).split(":")[0],
bufferMins: convertMinsToHrsMins(user.bufferTime).split(":")[1],
});
}, [formMethods, queryMe.data]);
if (queryMe.status === "loading") {
return <Loader />;
@ -40,86 +61,6 @@ export default function Availability() {
}
const user = queryMe.data;
function toggleAddModal() {
setShowAddModal(!showAddModal);
}
function toggleChangeTimesModal() {
setShowChangeTimesModal(!showChangeTimesModal);
}
const closeSuccessModal = () => {
setSuccessModalOpen(false);
router.replace(router.asPath);
};
function convertMinsToHrsMins(mins) {
let h = Math.floor(mins / 60);
let m = mins % 60;
h = h < 10 ? "0" + h : h;
m = m < 10 ? "0" + m : m;
return `${h}:${m}`;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function createEventTypeHandler(event) {
event.preventDefault();
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = lengthRef.current.value;
const enteredIsHidden = isHiddenRef.current.checked;
// TODO: Add validation
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const response = await fetch("/api/availability/eventtype", {
method: "POST",
body: JSON.stringify({
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
hidden: enteredIsHidden,
}),
headers: {
"Content-Type": "application/json",
},
});
if (enteredTitle && enteredLength) {
router.replace(router.asPath);
toggleAddModal();
}
}
async function updateStartEndTimesHandler(event) {
event.preventDefault();
const enteredStartHours = parseInt(startHoursRef.current.value);
const enteredStartMins = parseInt(startMinsRef.current.value);
const enteredEndHours = parseInt(endHoursRef.current.value);
const enteredEndMins = parseInt(endMinsRef.current.value);
const enteredBufferHours = parseInt(bufferHoursRef.current.value);
const enteredBufferMins = parseInt(bufferMinsRef.current.value);
const startMins = enteredStartHours * 60 + enteredStartMins;
const endMins = enteredEndHours * 60 + enteredEndMins;
const bufferMins = enteredBufferHours * 60 + enteredBufferMins;
// TODO: Add validation
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const response = await fetch("/api/availability/day", {
method: "PATCH",
body: JSON.stringify({ start: startMins, end: endMins, buffer: bufferMins }),
headers: {
"Content-Type": "application/json",
},
});
setShowChangeTimesModal(false);
setSuccessModalOpen(true);
}
return (
<div>
<Shell heading="Availability" subtitle="Configure times when you are available for bookings.">
@ -136,9 +77,7 @@ export default function Availability() {
</p>
</div>
<div className="mt-5">
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-primary">
Change available times
</button>
<Button href={formModal.hrefOn}>Change available times</Button>
</div>
</div>
</div>
@ -159,153 +98,148 @@ export default function Availability() {
</div>
</div>
</div>
{showChangeTimesModal && (
<div
className="fixed z-50 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 z-0 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<ClockIcon className="h-6 w-6 text-neutral-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Change your available times
</h3>
<div>
<p className="text-sm text-gray-500">
Set the start and end time of your day and a minimum buffer between your meetings.
</p>
</div>
</div>
<Dialog
open={formModal.isOn}
onOpenChange={(isOpen) => {
router.push(isOpen ? formModal.hrefOn : formModal.hrefOff);
}}>
<DialogContent>
<div className="sm:flex sm:items-start mb-4">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<ClockIcon className="h-6 w-6 text-neutral-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Change your available times
</h3>
<div>
<p className="text-sm text-gray-500">
Set the start and end time of your day and a minimum buffer between your meetings.
</p>
</div>
<form onSubmit={updateStartEndTimesHandler}>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
<div>
<label htmlFor="hours" className="sr-only">
Hours
</label>
<input
ref={startHoursRef}
type="number"
name="hours"
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="9"
defaultValue={convertMinsToHrsMins(user.startTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">
Minutes
</label>
<input
ref={startMinsRef}
type="number"
name="minutes"
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
defaultValue={convertMinsToHrsMins(user.startTime).split(":")[1]}
/>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
<div>
<label htmlFor="hours" className="sr-only">
Hours
</label>
<input
ref={endHoursRef}
type="number"
name="hours"
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="17"
defaultValue={convertMinsToHrsMins(user.endTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">
Minutes
</label>
<input
ref={endMinsRef}
type="number"
name="minutes"
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
defaultValue={convertMinsToHrsMins(user.endTime).split(":")[1]}
/>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Buffer</label>
<div>
<label htmlFor="hours" className="sr-only">
Hours
</label>
<input
ref={bufferHoursRef}
type="number"
name="hours"
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="0"
defaultValue={convertMinsToHrsMins(user.bufferTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">
Minutes
</label>
<input
ref={bufferMinsRef}
type="number"
name="minutes"
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="10"
defaultValue={convertMinsToHrsMins(user.bufferTime).split(":")[1]}
/>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
Update
</button>
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
)}
<Modal
heading="Start and end times changed"
description="The start and end times for your day have been changed successfully."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
<form
onSubmit={formMethods.handleSubmit(async (values) => {
const startMins = parseInt(values.startHours) * 60 + parseInt(values.startMins);
const endMins = parseInt(values.endHours) * 60 + parseInt(values.endMins);
const bufferMins = parseInt(values.bufferHours) * 60 + parseInt(values.bufferMins);
// TODO: Add validation
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const response = await fetch("/api/availability/day", {
method: "PATCH",
body: JSON.stringify({ start: startMins, end: endMins, buffer: bufferMins }),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
showToast("Something went wrong", "error");
return;
}
await queryMe.refetch();
router.push(formModal.hrefOff);
showToast("The start and end times for your day have been changed successfully.", "success");
})}>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
<div>
<label htmlFor="startHours" className="sr-only">
Hours
</label>
<input
{...formMethods.register("startHours")}
id="startHours"
type="number"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="9"
defaultValue={convertMinsToHrsMins(user.startTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="startMins" className="sr-only">
Minutes
</label>
<input
{...formMethods.register("startMins")}
id="startMins"
type="number"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
/>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
<div>
<label htmlFor="endHours" className="sr-only">
Hours
</label>
<input
{...formMethods.register("endHours")}
type="number"
id="endHours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="17"
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="endMins" className="sr-only">
Minutes
</label>
<input
{...formMethods.register("endMins")}
type="number"
id="endMins"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
/>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Buffer</label>
<div>
<label htmlFor="bufferHours" className="sr-only">
Hours
</label>
<input
{...formMethods.register("bufferHours")}
type="number"
id="bufferHours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="0"
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="bufferMins" className="sr-only">
Minutes
</label>
<input
{...formMethods.register("bufferMins")}
type="number"
id="bufferMins"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="10"
/>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex space-x-2">
<Button href={formModal.hrefOff} color="secondary" tabIndex={-1}>
Cancel
</Button>
<Button type="submit" loading={formMethods.formState.isSubmitting}>
Update
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</Shell>
</div>
);

View File

@ -548,6 +548,11 @@
version "1.0.4"
resolved "https://registry.npmjs.org/@heroicons/react/-/react-1.0.4.tgz"
"@hookform/resolvers@^2.8.1":
version "2.8.1"
resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.1.tgz#0d4fdd25bdeb4b98bf4e177c63fc4efa173454dd"
integrity sha512-U5lgaCkvD+0e5X8iQmCHiF+jOqjTX6OHUA7zPdeIHI6xdAOoi3rH9MKNuwMwv5Hly2LL6XtDgDkS/k+YG9hOew==
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz"
@ -6462,6 +6467,11 @@ react-fit@^1.0.3:
detect-element-overflow "^1.2.0"
prop-types "^15.6.0"
react-hook-form@^7.16.1:
version "7.16.1"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.16.1.tgz#669046df378a71949e5cf8a2398cbe20d5cb27bc"
integrity sha512-kcLDmSmlyLUFx2UU5bG/o4+3NeK753fhKodJa8gkplXohGkpAq0/p+TR24OWjZmkEc3ES7ppC5v5d6KUk+fJTA==
react-hot-toast@^2.1.0:
version "2.1.1"
resolved "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.1.1.tgz"