Detect users browser locale for time format 12/24 hours (#1900)
* fix: adds new isBrowserLocal24h timeFormat util, uses in BookingPage * fix: adds new time format locale detector in available times * fix: removes 24h clock from availabletimes * chore: move timeFormat to lib util, add to payment page * chore: remove commented out is24h * fix: adds timeFormat to success page * fix: adds timeFormat to cancel page * fix: adds timeFormat to video meeting ended/not started pages * fix: removes added typo in success page * fix: reverts back to use of is24h Switch in available times / time options, renames timeFormat to detectBrowserTimeFormat to avoid collisions * fix: moves use uf isBrowserLocal24h() to clock util initClock() itself, by calling it only if no localStorage settings are set * chore: move back timeFormat props to line it was so no change * chore: remove empty line in timeOptions * chore: move back timeFormat where it was in TimeOptions props * chore: add back empty line before selectedTimeZone return * fix: reverts back to use of is24h in payments page * feat: adds browser locale as default in payment page in case there's no settings set by the customer * feat: adds browser locale as default in success page * fix: deconstruct props so eslint passes * fix: lint issue for extra empty line in meeting-ended uid page Co-authored-by: Agusti Fernandez <git@agusti.me> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
5eca42bb45
commit
b860a79d59
10 changed files with 37 additions and 22 deletions
|
@ -13,7 +13,7 @@ type Props = {
|
||||||
onToggle24hClock: (is24hClock: boolean) => void;
|
onToggle24hClock: (is24hClock: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimeOptions: FC<Props> = (props) => {
|
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const [is24hClock, setIs24hClock] = useState(false);
|
const [is24hClock, setIs24hClock] = useState(false);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -25,13 +25,12 @@ const TimeOptions: FC<Props> = (props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
|
if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
|
||||||
props.onSelectTimeZone(timeZone(selectedTimeZone));
|
onSelectTimeZone(timeZone(selectedTimeZone));
|
||||||
}
|
}
|
||||||
}, [selectedTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
const handle24hClockToggle = (is24hClock: boolean) => {
|
const handle24hClockToggle = (is24hClock: boolean) => {
|
||||||
setIs24hClock(is24hClock);
|
setIs24hClock(is24hClock);
|
||||||
props.onToggle24hClock(is24h(is24hClock));
|
onToggle24hClock(is24h(is24hClock));
|
||||||
};
|
};
|
||||||
|
|
||||||
return selectedTimeZone !== "" ? (
|
return selectedTimeZone !== "" ? (
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
|
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||||
|
|
||||||
import CustomBranding from "@components/CustomBranding";
|
import CustomBranding from "@components/CustomBranding";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||||
|
@ -62,11 +63,13 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||||
}, [router.query.date]);
|
}, [router.query.date]);
|
||||||
|
|
||||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
||||||
const [timeFormat, setTimeFormat] = useState("h:mma");
|
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
|
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
|
||||||
|
|
||||||
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
|
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
import { parseZone } from "@lib/parseZone";
|
import { parseZone } from "@lib/parseZone";
|
||||||
import slugify from "@lib/slugify";
|
import slugify from "@lib/slugify";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
|
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||||
|
|
||||||
import CustomBranding from "@components/CustomBranding";
|
import CustomBranding from "@components/CustomBranding";
|
||||||
import { EmailInput, Form } from "@components/form/fields";
|
import { EmailInput, Form } from "@components/form/fields";
|
||||||
|
@ -110,9 +111,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
|
|
||||||
const rescheduleUid = router.query.rescheduleUid as string;
|
const rescheduleUid = router.query.rescheduleUid as string;
|
||||||
const { isReady, Theme } = useTheme(props.profile.theme);
|
const { isReady, Theme } = useTheme(props.profile.theme);
|
||||||
|
|
||||||
const date = asStringOrNull(router.query.date);
|
const date = asStringOrNull(router.query.date);
|
||||||
const timeFormat = asStringOrNull(router.query.clock) === "24h" ? "H:mm" : "h:mma";
|
|
||||||
|
|
||||||
const [guestToggle, setGuestToggle] = useState(props.booking && props.booking.attendees.length > 1);
|
const [guestToggle, setGuestToggle] = useState(props.booking && props.booking.attendees.length > 1);
|
||||||
|
|
||||||
|
@ -213,7 +212,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
if (!date) return "No date";
|
if (!date) return "No date";
|
||||||
const parsedZone = parseZone(date);
|
const parsedZone = parseZone(date);
|
||||||
if (!parsedZone?.isValid()) return "Invalid date";
|
if (!parsedZone?.isValid()) return "Invalid date";
|
||||||
const formattedTime = parsedZone?.format(timeFormat);
|
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
|
||||||
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { PaymentPageProps } from "@ee/pages/payment/[uid]";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
|
import { isBrowserLocale24h } from "@lib/timeFormat";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(toArray);
|
dayjs.extend(toArray);
|
||||||
|
@ -21,7 +22,7 @@ dayjs.extend(timezone);
|
||||||
|
|
||||||
const PaymentPage: FC<PaymentPageProps> = (props) => {
|
const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [is24h, setIs24h] = useState(false);
|
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
||||||
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
|
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
|
||||||
const { isReady, Theme } = useTheme(props.profile.theme);
|
const { isReady, Theme } = useTheme(props.profile.theme);
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import dayjs from "dayjs";
|
||||||
import timezone from "dayjs/plugin/timezone";
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
|
import { isBrowserLocale24h } from "./timeFormat";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
@ -22,6 +24,8 @@ const initClock = () => {
|
||||||
if (typeof localStorage === "undefined" || isInitialized) {
|
if (typeof localStorage === "undefined" || isInitialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// This only sets browser locale if there's no preference on localStorage.
|
||||||
|
if (!localStorage || !localStorage.getItem("timeOption.is24hClock")) set24hClock(isBrowserLocale24h());
|
||||||
timeOptions.is24hClock = localStorage.getItem("timeOption.is24hClock") === "true";
|
timeOptions.is24hClock = localStorage.getItem("timeOption.is24hClock") === "true";
|
||||||
timeOptions.inviteeTimeZone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess();
|
timeOptions.inviteeTimeZone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess();
|
||||||
};
|
};
|
||||||
|
|
12
apps/web/lib/timeFormat.ts
Normal file
12
apps/web/lib/timeFormat.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Detects navigator locale 24h time preference
|
||||||
|
* It works by checking whether hour output contains AM ('1 AM' or '01 h')
|
||||||
|
* based on the user's preferred language
|
||||||
|
* defaults to 'en-US' (12h) if no navigator language is found
|
||||||
|
*/
|
||||||
|
export const isBrowserLocale24h = () => {
|
||||||
|
let locale = "en-US";
|
||||||
|
if (process.browser && navigator) locale = navigator?.language;
|
||||||
|
return !new Intl.DateTimeFormat(locale, { hour: "numeric" }).format(0).match(/AM/);
|
||||||
|
};
|
||||||
|
export const detectBrowserTimeFormat = isBrowserLocale24h() ? "H:mm" : "h:mma";
|
|
@ -9,6 +9,7 @@ import { getSession } from "@lib/auth";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
|
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CustomBranding from "@components/CustomBranding";
|
import CustomBranding from "@components/CustomBranding";
|
||||||
|
@ -23,7 +24,6 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
// Get router variables
|
// Get router variables
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { uid } = router.query;
|
const { uid } = router.query;
|
||||||
const [is24h] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
|
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
|
||||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||||
|
@ -84,7 +84,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
<p className="text-gray-500">
|
<p className="text-gray-500">
|
||||||
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||||
{dayjs(props.booking?.startTime).format(
|
{dayjs(props.booking?.startTime).format(
|
||||||
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
|
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
import { isBrowserLocale24h } from "@lib/timeFormat";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CustomBranding from "@components/CustomBranding";
|
import CustomBranding from "@components/CustomBranding";
|
||||||
|
@ -34,8 +35,8 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { location: _location, name, reschedule } = router.query;
|
const { location: _location, name, reschedule } = router.query;
|
||||||
const location = Array.isArray(_location) ? _location[0] : _location;
|
const location = Array.isArray(_location) ? _location[0] : _location;
|
||||||
|
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
||||||
|
|
||||||
const [is24h, setIs24h] = useState(false);
|
|
||||||
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
|
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
|
||||||
const { isReady, Theme } = useTheme(props.profile.theme);
|
const { isReady, Theme } = useTheme(props.profile.theme);
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@ import dayjs from "dayjs";
|
||||||
import { NextPageContext } from "next";
|
import { NextPageContext } from "next";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
|
@ -15,10 +15,7 @@ import Button from "@components/ui/Button";
|
||||||
|
|
||||||
export default function MeetingUnavailable(props: inferSSRProps<typeof getServerSideProps>) {
|
export default function MeetingUnavailable(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// if no booking redirectis to the 404 page
|
||||||
const [is24h, setIs24h] = useState(false);
|
|
||||||
|
|
||||||
//if no booking redirectis to the 404 page
|
|
||||||
const emptyBooking = props.booking === null;
|
const emptyBooking = props.booking === null;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emptyBooking) {
|
if (emptyBooking) {
|
||||||
|
@ -57,7 +54,7 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
|
||||||
<p className="text-center text-gray-500">
|
<p className="text-center text-gray-500">
|
||||||
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||||
{dayjs(props.booking.startTime).format(
|
{dayjs(props.booking.startTime).format(
|
||||||
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
|
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,10 +4,10 @@ import dayjs from "dayjs";
|
||||||
import { NextPageContext } from "next";
|
import { NextPageContext } from "next";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
|
@ -23,7 +23,6 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
|
||||||
router.push("/video/no-meeting-found");
|
router.push("/video/no-meeting-found");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [is24h, setIs24h] = useState(false);
|
|
||||||
if (!emptyBooking) {
|
if (!emptyBooking) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -56,7 +55,7 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
|
||||||
<p className="text-center text-gray-500">
|
<p className="text-center text-gray-500">
|
||||||
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||||
{dayjs(props.booking.startTime).format(
|
{dayjs(props.booking.startTime).format(
|
||||||
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
|
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue