Let users set 12/24 hour time format (#2002)
This commit is contained in:
		
							parent
							
								
									2559873b2c
								
							
						
					
					
						commit
						7826a34b00
					
				
					 8 changed files with 53 additions and 5 deletions
				
			
		|  | @ -9,6 +9,7 @@ import { useLocale } from "@lib/hooks/useLocale"; | ||||||
| import { inferQueryOutput, trpc } from "@lib/trpc"; | import { inferQueryOutput, trpc } from "@lib/trpc"; | ||||||
| 
 | 
 | ||||||
| import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@components/Dialog"; | import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@components/Dialog"; | ||||||
|  | import { useMeQuery } from "@components/Shell"; | ||||||
| import { TextArea } from "@components/form/fields"; | import { TextArea } from "@components/form/fields"; | ||||||
| import Button from "@components/ui/Button"; | import Button from "@components/ui/Button"; | ||||||
| import TableActions, { ActionType } from "@components/ui/TableActions"; | import TableActions, { ActionType } from "@components/ui/TableActions"; | ||||||
|  | @ -16,6 +17,9 @@ import TableActions, { ActionType } from "@components/ui/TableActions"; | ||||||
| type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number]; | type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number]; | ||||||
| 
 | 
 | ||||||
| function BookingListItem(booking: BookingItem) { | function BookingListItem(booking: BookingItem) { | ||||||
|  |   // Get user so we can determine 12/24 hour format preferences
 | ||||||
|  |   const query = useMeQuery(); | ||||||
|  |   const user = query.data; | ||||||
|   const { t, i18n } = useLocale(); |   const { t, i18n } = useLocale(); | ||||||
|   const utils = trpc.useContext(); |   const utils = trpc.useContext(); | ||||||
|   const [rejectionReason, setRejectionReason] = useState<string>(""); |   const [rejectionReason, setRejectionReason] = useState<string>(""); | ||||||
|  | @ -120,7 +124,8 @@ function BookingListItem(booking: BookingItem) { | ||||||
|         <td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell"> |         <td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell"> | ||||||
|           <div className="text-sm leading-6 text-gray-900">{startTime}</div> |           <div className="text-sm leading-6 text-gray-900">{startTime}</div> | ||||||
|           <div className="text-sm text-gray-500"> |           <div className="text-sm text-gray-500"> | ||||||
|             {dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} |             {dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "} | ||||||
|  |             {dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} | ||||||
|           </div> |           </div> | ||||||
|         </td> |         </td> | ||||||
|         <td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}> |         <td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}> | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import { weekdayNames } from "@lib/core/i18n/weekday"; | ||||||
| import { useLocale } from "@lib/hooks/useLocale"; | import { useLocale } from "@lib/hooks/useLocale"; | ||||||
| import { TimeRange } from "@lib/types/schedule"; | import { TimeRange } from "@lib/types/schedule"; | ||||||
| 
 | 
 | ||||||
|  | import { useMeQuery } from "@components/Shell"; | ||||||
| import Button from "@components/ui/Button"; | import Button from "@components/ui/Button"; | ||||||
| import Select from "@components/ui/form/Select"; | import Select from "@components/ui/form/Select"; | ||||||
| 
 | 
 | ||||||
|  | @ -46,6 +47,10 @@ type TimeRangeFieldProps = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | ||||||
|  |   // Get user so we can determine 12/24 hour format preferences
 | ||||||
|  |   const query = useMeQuery(); | ||||||
|  |   const user = query.data; | ||||||
|  | 
 | ||||||
|   // Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
 |   // Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
 | ||||||
|   const [options, setOptions] = useState<Option[]>([]); |   const [options, setOptions] = useState<Option[]>([]); | ||||||
|   const [selected, setSelected] = useState<number | undefined>(); |   const [selected, setSelected] = useState<number | undefined>(); | ||||||
|  | @ -57,7 +62,9 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | ||||||
| 
 | 
 | ||||||
|   const getOption = (time: ConfigType) => ({ |   const getOption = (time: ConfigType) => ({ | ||||||
|     value: dayjs(time).toDate().valueOf(), |     value: dayjs(time).toDate().valueOf(), | ||||||
|     label: dayjs(time).utc().format("HH:mm"), |     label: dayjs(time) | ||||||
|  |       .utc() | ||||||
|  |       .format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm"), | ||||||
|     // .toLocaleTimeString(i18n.language, { minute: "numeric", hour: "numeric" }),
 |     // .toLocaleTimeString(i18n.language, { minute: "numeric", hour: "numeric" }),
 | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  | @ -82,7 +89,7 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | ||||||
|           handleSelected(value); |           handleSelected(value); | ||||||
|           return ( |           return ( | ||||||
|             <Select |             <Select | ||||||
|               className="w-[6rem]" |               className="w-30" | ||||||
|               options={options} |               options={options} | ||||||
|               onFocus={() => setOptions(timeOptions())} |               onFocus={() => setOptions(timeOptions())} | ||||||
|               onBlur={() => setOptions([])} |               onBlur={() => setOptions([])} | ||||||
|  | @ -100,7 +107,7 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | ||||||
|         name={`${name}.end`} |         name={`${name}.end`} | ||||||
|         render={({ field: { onChange, value } }) => ( |         render={({ field: { onChange, value } }) => ( | ||||||
|           <Select |           <Select | ||||||
|             className="w-[6rem]" |             className="w-30" | ||||||
|             options={options} |             options={options} | ||||||
|             onFocus={() => setOptions(timeOptions({ selected }))} |             onFocus={() => setOptions(timeOptions({ selected }))} | ||||||
|             onBlur={() => setOptions([])} |             onBlur={() => setOptions([])} | ||||||
|  |  | ||||||
|  | @ -146,6 +146,11 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | ||||||
|     { value: "light", label: t("light") }, |     { value: "light", label: t("light") }, | ||||||
|     { value: "dark", label: t("dark") }, |     { value: "dark", label: t("dark") }, | ||||||
|   ]; |   ]; | ||||||
|  | 
 | ||||||
|  |   const timeFormatOptions = [ | ||||||
|  |     { value: 12, label: t("12_hour") }, | ||||||
|  |     { value: 24, label: t("24_hour") }, | ||||||
|  |   ]; | ||||||
|   const usernameRef = useRef<HTMLInputElement>(null!); |   const usernameRef = useRef<HTMLInputElement>(null!); | ||||||
|   const nameRef = useRef<HTMLInputElement>(null!); |   const nameRef = useRef<HTMLInputElement>(null!); | ||||||
|   const emailRef = useRef<HTMLInputElement>(null!); |   const emailRef = useRef<HTMLInputElement>(null!); | ||||||
|  | @ -153,6 +158,10 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | ||||||
|   const avatarRef = useRef<HTMLInputElement>(null!); |   const avatarRef = useRef<HTMLInputElement>(null!); | ||||||
|   const hideBrandingRef = useRef<HTMLInputElement>(null!); |   const hideBrandingRef = useRef<HTMLInputElement>(null!); | ||||||
|   const [selectedTheme, setSelectedTheme] = useState<typeof themeOptions[number] | undefined>(); |   const [selectedTheme, setSelectedTheme] = useState<typeof themeOptions[number] | undefined>(); | ||||||
|  |   const [selectedTimeFormat, setSelectedTimeFormat] = useState({ | ||||||
|  |     value: props.user.timeFormat || 12, | ||||||
|  |     label: timeFormatOptions.find((option) => option.value === props.user.timeFormat)?.label || 12, | ||||||
|  |   }); | ||||||
|   const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(props.user.timeZone); |   const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(props.user.timeZone); | ||||||
|   const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ |   const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ | ||||||
|     value: props.user.weekStart, |     value: props.user.weekStart, | ||||||
|  | @ -189,6 +198,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | ||||||
|     const enteredWeekStartDay = selectedWeekStartDay.value; |     const enteredWeekStartDay = selectedWeekStartDay.value; | ||||||
|     const enteredHideBranding = hideBrandingRef.current.checked; |     const enteredHideBranding = hideBrandingRef.current.checked; | ||||||
|     const enteredLanguage = selectedLanguage.value; |     const enteredLanguage = selectedLanguage.value; | ||||||
|  |     const enteredTimeFormat = selectedTimeFormat.value; | ||||||
| 
 | 
 | ||||||
|     // TODO: Add validation
 |     // TODO: Add validation
 | ||||||
| 
 | 
 | ||||||
|  | @ -204,6 +214,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | ||||||
|       theme: asStringOrNull(selectedTheme?.value), |       theme: asStringOrNull(selectedTheme?.value), | ||||||
|       brandColor: enteredBrandColor, |       brandColor: enteredBrandColor, | ||||||
|       locale: enteredLanguage, |       locale: enteredLanguage, | ||||||
|  |       timeFormat: enteredTimeFormat, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -347,6 +358,21 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | ||||||
|                 /> |                 /> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |             <div> | ||||||
|  |               <label htmlFor="timeFormat" className="block text-sm font-medium text-gray-700"> | ||||||
|  |                 {t("time_format")} | ||||||
|  |               </label> | ||||||
|  |               <div className="mt-1"> | ||||||
|  |                 <Select | ||||||
|  |                   id="timeFormatSelect" | ||||||
|  |                   value={selectedTimeFormat || props.user.timeFormat} | ||||||
|  |                   onChange={(v) => v && setSelectedTimeFormat(v)} | ||||||
|  |                   classNamePrefix="react-select" | ||||||
|  |                   className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 capitalize shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm" | ||||||
|  |                   options={timeFormatOptions} | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|             <div> |             <div> | ||||||
|               <label htmlFor="weekStart" className="block text-sm font-medium text-gray-700"> |               <label htmlFor="weekStart" className="block text-sm font-medium text-gray-700"> | ||||||
|                 {t("first_day_of_week")} |                 {t("first_day_of_week")} | ||||||
|  | @ -499,6 +525,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => | ||||||
|       plan: true, |       plan: true, | ||||||
|       brandColor: true, |       brandColor: true, | ||||||
|       metadata: true, |       metadata: true, | ||||||
|  |       timeFormat: true, | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -659,5 +659,8 @@ | ||||||
|   "contact_sales": "Contact Sales", |   "contact_sales": "Contact Sales", | ||||||
|   "error_404": "Error 404", |   "error_404": "Error 404", | ||||||
|   "requires_ownership_of_a_token": "Requires ownership of a token belonging to the following address:", |   "requires_ownership_of_a_token": "Requires ownership of a token belonging to the following address:", | ||||||
|   "example_name": "John Doe" |   "example_name": "John Doe", | ||||||
|  |   "time_format": "Time format", | ||||||
|  |   "12_hour": "12 hour", | ||||||
|  |   "24_hour": "24 hour" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -66,6 +66,7 @@ async function getUserFromSession({ | ||||||
|       completedOnboarding: true, |       completedOnboarding: true, | ||||||
|       destinationCalendar: true, |       destinationCalendar: true, | ||||||
|       locale: true, |       locale: true, | ||||||
|  |       timeFormat: true, | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -75,6 +75,7 @@ const loggedInViewerRouter = createProtectedRouter() | ||||||
|         endTime: user.endTime, |         endTime: user.endTime, | ||||||
|         bufferTime: user.bufferTime, |         bufferTime: user.bufferTime, | ||||||
|         locale: user.locale, |         locale: user.locale, | ||||||
|  |         timeFormat: user.timeFormat, | ||||||
|         avatar: user.avatar, |         avatar: user.avatar, | ||||||
|         createdDate: user.createdDate, |         createdDate: user.createdDate, | ||||||
|         completedOnboarding: user.completedOnboarding, |         completedOnboarding: user.completedOnboarding, | ||||||
|  | @ -612,6 +613,7 @@ const loggedInViewerRouter = createProtectedRouter() | ||||||
|       theme: z.string().optional().nullable(), |       theme: z.string().optional().nullable(), | ||||||
|       completedOnboarding: z.boolean().optional(), |       completedOnboarding: z.boolean().optional(), | ||||||
|       locale: z.string().optional(), |       locale: z.string().optional(), | ||||||
|  |       timeFormat: z.number().optional(), | ||||||
|     }), |     }), | ||||||
|     async resolve({ input, ctx }) { |     async resolve({ input, ctx }) { | ||||||
|       const { user, prisma } = ctx; |       const { user, prisma } = ctx; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | -- AlterTable | ||||||
|  | ALTER TABLE "users" ADD COLUMN     "timeFormat" INTEGER DEFAULT 12; | ||||||
|  | @ -128,6 +128,7 @@ model User { | ||||||
|   selectedCalendars   SelectedCalendar[] |   selectedCalendars   SelectedCalendar[] | ||||||
|   completedOnboarding Boolean              @default(false) |   completedOnboarding Boolean              @default(false) | ||||||
|   locale              String? |   locale              String? | ||||||
|  |   timeFormat          Int?                 @default(12) | ||||||
|   twoFactorSecret     String? |   twoFactorSecret     String? | ||||||
|   twoFactorEnabled    Boolean              @default(false) |   twoFactorEnabled    Boolean              @default(false) | ||||||
|   identityProvider    IdentityProvider     @default(CAL) |   identityProvider    IdentityProvider     @default(CAL) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Bailey Pumfleet
						Bailey Pumfleet