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 { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@components/Dialog"; | ||||
| import { useMeQuery } from "@components/Shell"; | ||||
| import { TextArea } from "@components/form/fields"; | ||||
| import Button from "@components/ui/Button"; | ||||
| 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]; | ||||
| 
 | ||||
| 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 utils = trpc.useContext(); | ||||
|   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"> | ||||
|           <div className="text-sm leading-6 text-gray-900">{startTime}</div> | ||||
|           <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> | ||||
|         </td> | ||||
|         <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 { TimeRange } from "@lib/types/schedule"; | ||||
| 
 | ||||
| import { useMeQuery } from "@components/Shell"; | ||||
| import Button from "@components/ui/Button"; | ||||
| import Select from "@components/ui/form/Select"; | ||||
| 
 | ||||
|  | @ -46,6 +47,10 @@ type 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.
 | ||||
|   const [options, setOptions] = useState<Option[]>([]); | ||||
|   const [selected, setSelected] = useState<number | undefined>(); | ||||
|  | @ -57,7 +62,9 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | |||
| 
 | ||||
|   const getOption = (time: ConfigType) => ({ | ||||
|     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" }),
 | ||||
|   }); | ||||
| 
 | ||||
|  | @ -82,7 +89,7 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | |||
|           handleSelected(value); | ||||
|           return ( | ||||
|             <Select | ||||
|               className="w-[6rem]" | ||||
|               className="w-30" | ||||
|               options={options} | ||||
|               onFocus={() => setOptions(timeOptions())} | ||||
|               onBlur={() => setOptions([])} | ||||
|  | @ -100,7 +107,7 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { | |||
|         name={`${name}.end`} | ||||
|         render={({ field: { onChange, value } }) => ( | ||||
|           <Select | ||||
|             className="w-[6rem]" | ||||
|             className="w-30" | ||||
|             options={options} | ||||
|             onFocus={() => setOptions(timeOptions({ selected }))} | ||||
|             onBlur={() => setOptions([])} | ||||
|  |  | |||
|  | @ -146,6 +146,11 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | |||
|     { value: "light", label: t("light") }, | ||||
|     { 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 nameRef = 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 hideBrandingRef = useRef<HTMLInputElement>(null!); | ||||
|   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 [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ | ||||
|     value: props.user.weekStart, | ||||
|  | @ -189,6 +198,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | |||
|     const enteredWeekStartDay = selectedWeekStartDay.value; | ||||
|     const enteredHideBranding = hideBrandingRef.current.checked; | ||||
|     const enteredLanguage = selectedLanguage.value; | ||||
|     const enteredTimeFormat = selectedTimeFormat.value; | ||||
| 
 | ||||
|     // TODO: Add validation
 | ||||
| 
 | ||||
|  | @ -204,6 +214,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | |||
|       theme: asStringOrNull(selectedTheme?.value), | ||||
|       brandColor: enteredBrandColor, | ||||
|       locale: enteredLanguage, | ||||
|       timeFormat: enteredTimeFormat, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | @ -347,6 +358,21 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str | |||
|                 /> | ||||
|               </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> | ||||
|               <label htmlFor="weekStart" className="block text-sm font-medium text-gray-700"> | ||||
|                 {t("first_day_of_week")} | ||||
|  | @ -499,6 +525,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => | |||
|       plan: true, | ||||
|       brandColor: true, | ||||
|       metadata: true, | ||||
|       timeFormat: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -659,5 +659,8 @@ | |||
|   "contact_sales": "Contact Sales", | ||||
|   "error_404": "Error 404", | ||||
|   "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, | ||||
|       destinationCalendar: true, | ||||
|       locale: true, | ||||
|       timeFormat: true, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -75,6 +75,7 @@ const loggedInViewerRouter = createProtectedRouter() | |||
|         endTime: user.endTime, | ||||
|         bufferTime: user.bufferTime, | ||||
|         locale: user.locale, | ||||
|         timeFormat: user.timeFormat, | ||||
|         avatar: user.avatar, | ||||
|         createdDate: user.createdDate, | ||||
|         completedOnboarding: user.completedOnboarding, | ||||
|  | @ -612,6 +613,7 @@ const loggedInViewerRouter = createProtectedRouter() | |||
|       theme: z.string().optional().nullable(), | ||||
|       completedOnboarding: z.boolean().optional(), | ||||
|       locale: z.string().optional(), | ||||
|       timeFormat: z.number().optional(), | ||||
|     }), | ||||
|     async resolve({ input, 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[] | ||||
|   completedOnboarding Boolean              @default(false) | ||||
|   locale              String? | ||||
|   timeFormat          Int?                 @default(12) | ||||
|   twoFactorSecret     String? | ||||
|   twoFactorEnabled    Boolean              @default(false) | ||||
|   identityProvider    IdentityProvider     @default(CAL) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Bailey Pumfleet
						Bailey Pumfleet