 8664d217c9
			
		
	
	
		8664d217c9
		
			
		
	
	
	
	
		
			
			* Refactored Schedule component * Merge branch 'main' into feature/availability-page-revamp * wip * Turned value into number, many other TS tweaks * NodeJS 16x works 100% on my local, but out of scope for this already massive PR * Fixed TS errors in viewer.tsx and schedule/index.ts * Reverted next.config.js * Fixed minor remnant from moving types to @lib/types * schema comment * some changes to form handling * add comments * Turned ConfigType into number; which seems to be the value preferred by tRPC * Fixed localized time display during onboarding * Update components/ui/form/Schedule.tsx Co-authored-by: Alex Johansson <alexander@n1s.se> * Added showToast to indicate save success * Converted number to Date, and also always establish time based on current date * prevent height flickering of availability by removing mb-2 of input field * availabilty: re-added mb-2 but added min-height * Quite a few bugs discovered, but this seems functional Co-authored-by: KATT <alexander@n1s.se> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
		
			
				
	
	
		
			187 lines
		
	
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { PlusIcon, TrashIcon } from "@heroicons/react/outline";
 | |
| import dayjs, { Dayjs } from "dayjs";
 | |
| import React, { useCallback, useState } from "react";
 | |
| import { Controller, useFieldArray } from "react-hook-form";
 | |
| 
 | |
| import { weekdayNames } from "@lib/core/i18n/weekday";
 | |
| import { useLocale } from "@lib/hooks/useLocale";
 | |
| import { TimeRange, Schedule as ScheduleType } from "@lib/types/schedule";
 | |
| 
 | |
| import Button from "@components/ui/Button";
 | |
| import Select from "@components/ui/form/Select";
 | |
| 
 | |
| /** Begin Time Increments For Select */
 | |
| const increment = 15;
 | |
| /**
 | |
|  * Creates an array of times on a 15 minute interval from
 | |
|  * 00:00:00 (Start of day) to
 | |
|  * 23:45:00 (End of day with enough time for 15 min booking)
 | |
|  */
 | |
| const TIMES = (() => {
 | |
|   const end = dayjs().endOf("day");
 | |
|   let t: Dayjs = dayjs().startOf("day");
 | |
| 
 | |
|   const times = [];
 | |
|   while (t.isBefore(end)) {
 | |
|     times.push(t);
 | |
|     t = t.add(increment, "minutes");
 | |
|   }
 | |
|   return times;
 | |
| })();
 | |
| /** End Time Increments For Select */
 | |
| 
 | |
| // sets the desired time in current date, needs to be current date for proper DST translation
 | |
| const defaultDayRange: TimeRange = {
 | |
|   start: new Date(new Date().setHours(9, 0, 0, 0)),
 | |
|   end: new Date(new Date().setHours(17, 0, 0, 0)),
 | |
| };
 | |
| 
 | |
| export const DEFAULT_SCHEDULE: ScheduleType = [
 | |
|   [],
 | |
|   [defaultDayRange],
 | |
|   [defaultDayRange],
 | |
|   [defaultDayRange],
 | |
|   [defaultDayRange],
 | |
|   [defaultDayRange],
 | |
|   [],
 | |
| ];
 | |
| 
 | |
| type Option = {
 | |
|   readonly label: string;
 | |
|   readonly value: number;
 | |
| };
 | |
| 
 | |
| type TimeRangeFieldProps = {
 | |
|   name: string;
 | |
| };
 | |
| 
 | |
| const TimeRangeField = ({ name }: TimeRangeFieldProps) => {
 | |
|   // Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
 | |
|   const [options, setOptions] = useState<Option[]>([]);
 | |
| 
 | |
|   const getOption = (time: Date) => ({
 | |
|     value: time.valueOf(),
 | |
|     label: time.toLocaleTimeString("nl-NL", { minute: "numeric", hour: "numeric" }),
 | |
|   });
 | |
| 
 | |
|   const timeOptions = useCallback((offsetOrLimit: { offset?: number; limit?: number } = {}) => {
 | |
|     const { limit, offset } = offsetOrLimit;
 | |
|     return TIMES.filter((time) => (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset))).map(
 | |
|       (t) => getOption(t.toDate())
 | |
|     );
 | |
|   }, []);
 | |
| 
 | |
|   return (
 | |
|     <>
 | |
|       <Controller
 | |
|         name={`${name}.start`}
 | |
|         render={({ field: { onChange, value } }) => (
 | |
|           <Select
 | |
|             className="w-[6rem]"
 | |
|             options={options}
 | |
|             onFocus={() => setOptions(timeOptions())}
 | |
|             onBlur={() => setOptions([])}
 | |
|             defaultValue={getOption(value)}
 | |
|             onChange={(option) => onChange(new Date(option?.value as number))}
 | |
|           />
 | |
|         )}
 | |
|       />
 | |
|       <span>-</span>
 | |
|       <Controller
 | |
|         name={`${name}.end`}
 | |
|         render={({ field: { onChange, value } }) => (
 | |
|           <Select
 | |
|             className="w-[6rem]"
 | |
|             options={options}
 | |
|             onFocus={() => setOptions(timeOptions())}
 | |
|             onBlur={() => setOptions([])}
 | |
|             defaultValue={getOption(value)}
 | |
|             onChange={(option) => onChange(new Date(option?.value as number))}
 | |
|           />
 | |
|         )}
 | |
|       />
 | |
|     </>
 | |
|   );
 | |
| };
 | |
| 
 | |
| type ScheduleBlockProps = {
 | |
|   day: number;
 | |
|   weekday: string;
 | |
|   name: string;
 | |
| };
 | |
| 
 | |
| const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
 | |
|   const { t } = useLocale();
 | |
|   const { fields, append, remove, replace } = useFieldArray({
 | |
|     name: `${name}.${day}`,
 | |
|   });
 | |
| 
 | |
|   const handleAppend = () => {
 | |
|     // FIXME: Fix type-inference, can't get this to work. @see https://github.com/react-hook-form/react-hook-form/issues/4499
 | |
|     const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end);
 | |
|     const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour");
 | |
| 
 | |
|     if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) {
 | |
|       return append({
 | |
|         start: nextRangeStart.toDate(),
 | |
|         end: nextRangeEnd.toDate(),
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <fieldset className="flex justify-between py-5 min-h-[86px]">
 | |
|       <div className="w-1/3">
 | |
|         <label className="flex items-center space-x-2">
 | |
|           <input
 | |
|             type="checkbox"
 | |
|             checked={fields.length > 0}
 | |
|             onChange={(e) => (e.target.checked ? replace([defaultDayRange]) : replace([]))}
 | |
|             className="inline-block border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900"
 | |
|           />
 | |
|           <span className="inline-block capitalize">{weekday}</span>
 | |
|         </label>
 | |
|       </div>
 | |
|       <div className="flex-grow">
 | |
|         {fields.map((field, index) => (
 | |
|           <div key={field.id} className="flex justify-between mb-2">
 | |
|             <div className="flex items-center space-x-2">
 | |
|               <TimeRangeField name={`${name}.${day}.${index}`} />
 | |
|             </div>
 | |
|             <Button
 | |
|               size="icon"
 | |
|               color="minimal"
 | |
|               StartIcon={TrashIcon}
 | |
|               type="button"
 | |
|               onClick={() => remove(index)}
 | |
|             />
 | |
|           </div>
 | |
|         ))}
 | |
|         {!fields.length && t("no_availability")}
 | |
|       </div>
 | |
|       <div>
 | |
|         <Button
 | |
|           type="button"
 | |
|           color="minimal"
 | |
|           size="icon"
 | |
|           className={fields.length > 0 ? "visible" : "invisible"}
 | |
|           StartIcon={PlusIcon}
 | |
|           onClick={handleAppend}
 | |
|         />
 | |
|       </div>
 | |
|     </fieldset>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const Schedule = ({ name }: { name: string }) => {
 | |
|   const { i18n } = useLocale();
 | |
|   return (
 | |
|     <fieldset className="divide-y divide-gray-200">
 | |
|       {weekdayNames(i18n.language).map((weekday, num) => (
 | |
|         <ScheduleBlock key={num} name={name} weekday={weekday} day={num} />
 | |
|       ))}
 | |
|     </fieldset>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default Schedule;
 |