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