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:
Agusti Fernandez 2022-02-23 13:37:15 +01:00 committed by GitHub
parent 5eca42bb45
commit b860a79d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 37 additions and 22 deletions

View file

@ -13,7 +13,7 @@ type Props = {
onToggle24hClock: (is24hClock: boolean) => void;
};
const TimeOptions: FC<Props> = (props) => {
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone }) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [is24hClock, setIs24hClock] = useState(false);
const { t } = useLocale();
@ -25,13 +25,12 @@ const TimeOptions: FC<Props> = (props) => {
useEffect(() => {
if (selectedTimeZone && timeZone() && selectedTimeZone !== timeZone()) {
props.onSelectTimeZone(timeZone(selectedTimeZone));
onSelectTimeZone(timeZone(selectedTimeZone));
}
}, [selectedTimeZone]);
}, [selectedTimeZone, onSelectTimeZone]);
const handle24hClockToggle = (is24hClock: boolean) => {
setIs24hClock(is24hClock);
props.onToggle24hClock(is24h(is24hClock));
onToggle24hClock(is24h(is24hClock));
};
return selectedTimeZone !== "" ? (

View file

@ -15,6 +15,7 @@ import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import CustomBranding from "@components/CustomBranding";
import AvailableTimes from "@components/booking/AvailableTimes";
@ -62,11 +63,13 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
}, [router.query.date]);
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
const [timeFormat, setTimeFormat] = useState("h:mma");
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
const telemetry = useTelemetry();
useEffect(() => {
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
}, [telemetry]);

View file

@ -24,6 +24,7 @@ import createBooking from "@lib/mutations/bookings/create-booking";
import { parseZone } from "@lib/parseZone";
import slugify from "@lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import CustomBranding from "@components/CustomBranding";
import { EmailInput, Form } from "@components/form/fields";
@ -110,9 +111,7 @@ const BookingPage = (props: BookingPageProps) => {
const rescheduleUid = router.query.rescheduleUid as string;
const { isReady, Theme } = useTheme(props.profile.theme);
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);
@ -213,7 +212,7 @@ const BookingPage = (props: BookingPageProps) => {
if (!date) return "No date";
const parsedZone = parseZone(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" });
};

View file

@ -14,6 +14,7 @@ import { PaymentPageProps } from "@ee/pages/payment/[uid]";
import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import { isBrowserLocale24h } from "@lib/timeFormat";
dayjs.extend(utc);
dayjs.extend(toArray);
@ -21,7 +22,7 @@ dayjs.extend(timezone);
const PaymentPage: FC<PaymentPageProps> = (props) => {
const { t } = useLocale();
const [is24h, setIs24h] = useState(false);
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
const { isReady, Theme } = useTheme(props.profile.theme);

View file

@ -3,6 +3,8 @@ import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { isBrowserLocale24h } from "./timeFormat";
dayjs.extend(utc);
dayjs.extend(timezone);
@ -22,6 +24,8 @@ const initClock = () => {
if (typeof localStorage === "undefined" || isInitialized) {
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.inviteeTimeZone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess();
};

View 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";

View file

@ -9,6 +9,7 @@ import { getSession } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
@ -23,7 +24,6 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
// Get router variables
const router = useRouter();
const { uid } = router.query;
const [is24h] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
const [cancellationReason, setCancellationReason] = useState<string>("");
@ -84,7 +84,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
<p className="text-gray-500">
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking?.startTime).format(
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
)}
</p>
</div>

View file

@ -16,6 +16,7 @@ import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import prisma from "@lib/prisma";
import { isBrowserLocale24h } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
@ -34,8 +35,8 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
const router = useRouter();
const { location: _location, name, reschedule } = router.query;
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 { isReady, Theme } = useTheme(props.profile.theme);

View file

@ -4,10 +4,10 @@ import dayjs from "dayjs";
import { NextPageContext } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useEffect } from "react";
import prisma from "@lib/prisma";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
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>) {
const router = useRouter();
const [is24h, setIs24h] = useState(false);
//if no booking redirectis to the 404 page
// if no booking redirectis to the 404 page
const emptyBooking = props.booking === null;
useEffect(() => {
if (emptyBooking) {
@ -57,7 +54,7 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
<p className="text-center text-gray-500">
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
)}
</p>
</div>

View file

@ -4,10 +4,10 @@ import dayjs from "dayjs";
import { NextPageContext } from "next";
import { getSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useEffect } from "react";
import prisma from "@lib/prisma";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferSSRProps } from "@lib/types/inferSSRProps";
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");
}
});
const [is24h, setIs24h] = useState(false);
if (!emptyBooking) {
return (
<div>
@ -56,7 +55,7 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
<p className="text-center text-gray-500">
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(
(is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY"
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
)}
</p>
</div>