I18n's the i18n language dropdown & weekday using Intl (#955)
* I18n's the i18n language dropdown & weekday using Intl * Some type fixes * Trigger locale changes instantly (#958) * Trigger locale changes instantly * Restored types * Capitalize languages across the board Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>pull/938/head^2
parent
b5e176a87e
commit
ce8e9c126b
|
@ -44,31 +44,3 @@ export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promi
|
|||
|
||||
return preferredLocale;
|
||||
};
|
||||
|
||||
interface localeType {
|
||||
[locale: string]: string;
|
||||
}
|
||||
|
||||
export const localeLabels: localeType = {
|
||||
en: "English",
|
||||
fr: "French",
|
||||
it: "Italian",
|
||||
ru: "Russian",
|
||||
es: "Spanish",
|
||||
de: "German",
|
||||
pt: "Portuguese",
|
||||
ro: "Romanian",
|
||||
nl: "Dutch",
|
||||
"pt-BR": "Portuguese (Brazilian)",
|
||||
"es-419": "Spanish, Latin America",
|
||||
ko: "Korean",
|
||||
};
|
||||
|
||||
export type OptionType = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const localeOptions: OptionType[] = i18n.locales.map((locale) => {
|
||||
return { value: locale, label: localeLabels[locale] };
|
||||
});
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
|
||||
export function weekdayNames(locale: string | string[], weekStart = 0, type: "short" | "long" = "long") {
|
||||
return Array.from(Array(7).keys()).map((d) => nameOfDay(locale, d + weekStart, type));
|
||||
}
|
||||
|
||||
export function nameOfDay(locale: string | string[], day: number, type: "short" | "long" = "long") {
|
||||
return new Intl.DateTimeFormat(locale, { weekday: type }).format(new Date(1970, 0, day + 4));
|
||||
}
|
|
@ -1,18 +1,15 @@
|
|||
import { InformationCircleIcon } from "@heroicons/react/outline";
|
||||
import crypto from "crypto";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { RefObject, useEffect, useRef, useState } from "react";
|
||||
import Select from "react-select";
|
||||
import { i18n } from "next-i18next.config";
|
||||
import { ComponentProps, RefObject, useEffect, useRef, useState } from "react";
|
||||
import Select, { OptionTypeBase } from "react-select";
|
||||
import TimezoneSelect from "react-timezone-select";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import {
|
||||
getOrSetUserLocaleFromHeaders,
|
||||
localeLabels,
|
||||
localeOptions,
|
||||
OptionType,
|
||||
} from "@lib/core/i18n/i18n.utils";
|
||||
import { nameOfDay } from "@lib/core/i18n/weekday";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
import showToast from "@lib/notification";
|
||||
|
@ -20,7 +17,7 @@ import prisma from "@lib/prisma";
|
|||
import { trpc } from "@lib/trpc";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { DialogClose, Dialog, DialogContent } from "@components/Dialog";
|
||||
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
|
||||
import ImageUploader from "@components/ImageUploader";
|
||||
import SettingsShell from "@components/SettingsShell";
|
||||
import Shell from "@components/Shell";
|
||||
|
@ -31,6 +28,14 @@ import Button from "@components/ui/Button";
|
|||
import { UsernameInput } from "@components/ui/UsernameInput";
|
||||
|
||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
const getLocaleOptions = (displayLocale: string | string[]): OptionTypeBase[] => {
|
||||
return i18n.locales.map((locale) => ({
|
||||
value: locale,
|
||||
label: new Intl.DisplayNames(displayLocale, { type: "language" }).of(locale),
|
||||
}));
|
||||
};
|
||||
|
||||
function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>; user: Props["user"] }) {
|
||||
const { t } = useLocale();
|
||||
const [modelOpen, setModalOpen] = useState(false);
|
||||
|
@ -58,12 +63,12 @@ function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>
|
|||
/>
|
||||
<Dialog open={modelOpen}>
|
||||
<DialogContent>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100 mb-4">
|
||||
<InformationCircleIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />
|
||||
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-yellow-100 rounded-full">
|
||||
<InformationCircleIcon className="w-6 h-6 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="sm:flex sm:items-start mb-4">
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="mt-3 sm:mt-0 sm:text-left">
|
||||
<h3 className="font-cal text-lg leading-6 font-bold text-gray-900" id="modal-title">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900 font-cal" id="modal-title">
|
||||
{t("only_available_on_pro_plan")}
|
||||
</h3>
|
||||
</div>
|
||||
|
@ -83,7 +88,7 @@ function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>
|
|||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-x-2">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
className="btn-wide btn-primary text-center table-cell"
|
||||
className="table-cell text-center btn-wide btn-primary"
|
||||
onClick={() => setModalOpen(false)}>
|
||||
{t("dismiss")}
|
||||
</Button>
|
||||
|
@ -95,9 +100,25 @@ function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>
|
|||
);
|
||||
}
|
||||
|
||||
export default function Settings(props: Props) {
|
||||
function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: string }) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const mutation = trpc.useMutation("viewer.updateProfile");
|
||||
const mutation = trpc.useMutation("viewer.updateProfile", {
|
||||
onSuccess: () => {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
setHasErrors(false); // dismiss any open errors
|
||||
},
|
||||
onError: (err) => {
|
||||
setHasErrors(true);
|
||||
setErrorMessage(err.message);
|
||||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.invalidateQueries(["viewer.i18n"]);
|
||||
},
|
||||
});
|
||||
|
||||
const localeOptions = getLocaleOptions(props.localeProp);
|
||||
|
||||
const themeOptions = [
|
||||
{ value: "light", label: t("light") },
|
||||
|
@ -109,15 +130,16 @@ export default function Settings(props: Props) {
|
|||
const descriptionRef = useRef<HTMLTextAreaElement>(null!);
|
||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||
const hideBrandingRef = useRef<HTMLInputElement>(null!);
|
||||
const [selectedTheme, setSelectedTheme] = useState<undefined | { value: string; label: string }>(undefined);
|
||||
const [selectedTheme, setSelectedTheme] = useState<OptionTypeBase>();
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
|
||||
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({
|
||||
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState<OptionTypeBase>({
|
||||
value: props.user.weekStart,
|
||||
label: "",
|
||||
label: nameOfDay(props.localeProp, props.user.weekStart === "Sunday" ? 0 : 1),
|
||||
});
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<OptionType>({
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<OptionTypeBase>({
|
||||
value: props.localeProp,
|
||||
label: props.localeLabels[props.localeProp],
|
||||
label: localeOptions.find((option) => option.value === props.localeProp)?.label,
|
||||
});
|
||||
const [imageSrc, setImageSrc] = useState<string>(props.user.avatar || "");
|
||||
const [hasErrors, setHasErrors] = useState(false);
|
||||
|
@ -127,8 +149,6 @@ export default function Settings(props: Props) {
|
|||
setSelectedTheme(
|
||||
props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : undefined
|
||||
);
|
||||
setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart });
|
||||
setSelectedLanguage({ value: props.localeProp, label: props.localeLabels[props.localeProp] });
|
||||
}, []);
|
||||
|
||||
async function updateProfileHandler(event) {
|
||||
|
@ -145,274 +165,239 @@ export default function Settings(props: Props) {
|
|||
|
||||
// TODO: Add validation
|
||||
|
||||
await mutation
|
||||
.mutateAsync({
|
||||
username: enteredUsername,
|
||||
name: enteredName,
|
||||
bio: enteredDescription,
|
||||
avatar: enteredAvatar,
|
||||
timeZone: enteredTimeZone,
|
||||
weekStart: asStringOrUndefined(enteredWeekStartDay),
|
||||
hideBranding: enteredHideBranding,
|
||||
theme: asStringOrNull(selectedTheme?.value),
|
||||
locale: enteredLanguage,
|
||||
})
|
||||
.then(() => {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
setHasErrors(false); // dismiss any open errors
|
||||
})
|
||||
.catch((err) => {
|
||||
setHasErrors(true);
|
||||
setErrorMessage(err.message);
|
||||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
mutation.mutate({
|
||||
username: enteredUsername,
|
||||
name: enteredName,
|
||||
bio: enteredDescription,
|
||||
avatar: enteredAvatar,
|
||||
timeZone: enteredTimeZone,
|
||||
weekStart: asStringOrUndefined(enteredWeekStartDay),
|
||||
hideBranding: enteredHideBranding,
|
||||
theme: asStringOrNull(selectedTheme?.value),
|
||||
locale: enteredLanguage,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}>
|
||||
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
||||
<div className="py-6 lg:pb-8">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<div className="flex-grow space-y-6">
|
||||
<div className="block sm:flex">
|
||||
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
|
||||
<UsernameInput ref={usernameRef} defaultValue={props.user.username} />
|
||||
</div>
|
||||
<div className="w-full sm:w-1/2 sm:ml-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
{t("full_name")}
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
autoComplete="given-name"
|
||||
placeholder={t("your_name")}
|
||||
required
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
defaultValue={props.user.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="block sm:flex">
|
||||
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder={t("your_email")}
|
||||
disabled
|
||||
className="block w-full px-3 py-2 mt-1 text-gray-500 border border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm"
|
||||
defaultValue={props.user.email}
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||
{t("change_email_contact")}{" "}
|
||||
<a className="text-blue-500" href="mailto:help@cal.com">
|
||||
help@cal.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
||||
{t("about")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
ref={descriptionRef}
|
||||
id="about"
|
||||
name="about"
|
||||
placeholder={t("little_something_about")}
|
||||
rows={3}
|
||||
defaultValue={props.user.bio || undefined}
|
||||
className="block w-full mt-1 border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex mt-1">
|
||||
<Avatar
|
||||
displayName={props.user.name}
|
||||
className="relative w-10 h-10 rounded-full"
|
||||
gravatarFallbackMd5={props.user.emailMd5}
|
||||
imageSrc={imageSrc}
|
||||
/>
|
||||
<input
|
||||
ref={avatarRef}
|
||||
type="hidden"
|
||||
name="avatar"
|
||||
id="avatar"
|
||||
placeholder="URL"
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
defaultValue={imageSrc}
|
||||
/>
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("change_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
avatarRef.current.value = newAvatar;
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
"value"
|
||||
).set;
|
||||
nativeInputValueSetter.call(avatarRef.current, newAvatar);
|
||||
const ev2 = new Event("input", { bubbles: true });
|
||||
avatarRef.current.dispatchEvent(ev2);
|
||||
updateProfileHandler(ev2);
|
||||
setImageSrc(newAvatar);
|
||||
}}
|
||||
imageSrc={imageSrc}
|
||||
/>
|
||||
</div>
|
||||
<hr className="mt-6" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="language" className="block text-sm font-medium text-gray-700">
|
||||
{t("language")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Select
|
||||
id="languageSelect"
|
||||
value={selectedLanguage || props.localeProp}
|
||||
onChange={setSelectedLanguage}
|
||||
classNamePrefix="react-select"
|
||||
className="block w-full mt-1 capitalize border border-gray-300 rounded-sm shadow-sm react-select-container focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
options={localeOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={setSelectedTimeZone}
|
||||
classNamePrefix="react-select"
|
||||
className="block w-full mt-1 border border-gray-300 rounded-sm shadow-sm react-select-container focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="weekStart" className="block text-sm font-medium text-gray-700">
|
||||
{t("first_day_of_week")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Select
|
||||
id="weekStart"
|
||||
value={selectedWeekStartDay}
|
||||
onChange={setSelectedWeekStartDay}
|
||||
classNamePrefix="react-select"
|
||||
className="block w-full mt-1 capitalize border border-gray-300 rounded-sm shadow-sm react-select-container focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
options={[
|
||||
{ value: "Sunday", label: nameOfDay(props.localeProp, 0) },
|
||||
{ value: "Monday", label: nameOfDay(props.localeProp, 1) },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="theme" className="block text-sm font-medium text-gray-700">
|
||||
{t("single_theme")}
|
||||
</label>
|
||||
<div className="my-1">
|
||||
<Select
|
||||
id="theme"
|
||||
isDisabled={!selectedTheme}
|
||||
defaultValue={selectedTheme || themeOptions[0]}
|
||||
value={selectedTheme || themeOptions[0]}
|
||||
onChange={setSelectedTheme}
|
||||
className="shadow-sm | { value: string } focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||
options={themeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-start mt-8">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="theme-adjust-os"
|
||||
name="theme-adjust-os"
|
||||
type="checkbox"
|
||||
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
|
||||
checked={!selectedTheme}
|
||||
className="w-4 h-4 border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="theme-adjust-os" className="font-medium text-gray-700">
|
||||
{t("automatically_adjust_theme")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<HideBrandingInput user={props.user} hideBrandingRef={hideBrandingRef} />
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
||||
{t("disable_cal_branding")}{" "}
|
||||
{props.user.plan !== "PRO" && <Badge variant="default">PRO</Badge>}
|
||||
</label>
|
||||
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="flex justify-end py-4">
|
||||
<Button type="submit">{t("save")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.i18n"]);
|
||||
|
||||
return (
|
||||
<Shell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
|
||||
<SettingsShell>
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}>
|
||||
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
||||
<div className="py-6 lg:pb-8">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<div className="flex-grow space-y-6">
|
||||
<div className="block sm:flex">
|
||||
<div className="w-full sm:w-1/2 sm:mr-2 mb-6">
|
||||
<UsernameInput ref={usernameRef} defaultValue={props.user.username} />
|
||||
</div>
|
||||
<div className="w-full sm:w-1/2 sm:ml-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
{t("full_name")}
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
autoComplete="given-name"
|
||||
placeholder={t("your_name")}
|
||||
required
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
defaultValue={props.user.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="block sm:flex">
|
||||
<div className="w-full sm:w-1/2 sm:mr-2 mb-6">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder={t("your_email")}
|
||||
disabled
|
||||
className="mt-1 block w-full py-2 px-3 text-gray-500 border border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm"
|
||||
defaultValue={props.user.email}
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||
{t("change_email_contact")}{" "}
|
||||
<a className="text-blue-500" href="mailto:help@cal.com">
|
||||
help@cal.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
||||
{t("about")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
ref={descriptionRef}
|
||||
id="about"
|
||||
name="about"
|
||||
placeholder={t("little_something_about")}
|
||||
rows={3}
|
||||
defaultValue={props.user.bio}
|
||||
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mt-1 flex">
|
||||
<Avatar
|
||||
displayName={props.user.name}
|
||||
className="relative rounded-full w-10 h-10"
|
||||
gravatarFallbackMd5={props.user.emailMd5}
|
||||
imageSrc={imageSrc}
|
||||
/>
|
||||
<input
|
||||
ref={avatarRef}
|
||||
type="hidden"
|
||||
name="avatar"
|
||||
id="avatar"
|
||||
placeholder="URL"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
defaultValue={imageSrc}
|
||||
/>
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("change_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
avatarRef.current.value = newAvatar;
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
"value"
|
||||
).set;
|
||||
nativeInputValueSetter.call(avatarRef.current, newAvatar);
|
||||
const ev2 = new Event("input", { bubbles: true });
|
||||
avatarRef.current.dispatchEvent(ev2);
|
||||
updateProfileHandler(ev2);
|
||||
setImageSrc(newAvatar);
|
||||
}}
|
||||
imageSrc={imageSrc}
|
||||
/>
|
||||
</div>
|
||||
<hr className="mt-6" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="language" className="block text-sm font-medium text-gray-700">
|
||||
{t("language")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Select
|
||||
id="languageSelect"
|
||||
value={selectedLanguage || props.localeProp}
|
||||
onChange={setSelectedLanguage}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container border border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm"
|
||||
options={props.localeOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={setSelectedTimeZone}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container border border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="weekStart" className="block text-sm font-medium text-gray-700">
|
||||
{t("first_day_of_week")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Select
|
||||
id="weekStart"
|
||||
value={selectedWeekStartDay}
|
||||
onChange={setSelectedWeekStartDay}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container border border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm"
|
||||
options={[
|
||||
{ value: "Sunday", label: t("sunday") },
|
||||
{ value: "Monday", label: t("monday") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="theme" className="block text-sm font-medium text-gray-700">
|
||||
{t("single_theme")}
|
||||
</label>
|
||||
<div className="my-1">
|
||||
<Select
|
||||
id="theme"
|
||||
isDisabled={!selectedTheme}
|
||||
defaultValue={selectedTheme || themeOptions[0]}
|
||||
value={selectedTheme || themeOptions[0]}
|
||||
onChange={setSelectedTheme}
|
||||
className="shadow-sm | { value: string } focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||
options={themeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="theme-adjust-os"
|
||||
name="theme-adjust-os"
|
||||
type="checkbox"
|
||||
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
|
||||
checked={!selectedTheme}
|
||||
className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="theme-adjust-os" className="font-medium text-gray-700">
|
||||
{t("automatically_adjust_theme")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<HideBrandingInput user={props.user} hideBrandingRef={hideBrandingRef} />
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
||||
{t("disable_cal_branding")}{" "}
|
||||
{props.user.plan !== "PRO" && <Badge variant="default">PRO</Badge>}
|
||||
</label>
|
||||
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*<div className="mt-6 flex-grow lg:mt-0 lg:ml-6 lg:flex-grow-0 lg:flex-shrink-0">
|
||||
<p className="mb-2 text-sm font-medium text-gray-700" aria-hidden="true">
|
||||
Photo
|
||||
</p>
|
||||
<div className="mt-1 lg:hidden">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="flex-shrink-0 inline-block rounded-full overflow-hidden h-12 w-12"
|
||||
aria-hidden="true">
|
||||
<Avatar user={props.user} className="rounded-full h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden relative rounded-full overflow-hidden lg:block">
|
||||
<Avatar
|
||||
user={props.user}
|
||||
className="relative rounded-full w-40 h-40"
|
||||
fallback={<div className="relative bg-neutral-900 rounded-full w-40 h-40"></div>}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<label htmlFor="avatar" className="block text-sm font-medium text-gray-700">
|
||||
Avatar URL
|
||||
</label>
|
||||
<input
|
||||
ref={avatarRef}
|
||||
type="text"
|
||||
name="avatar"
|
||||
id="avatar"
|
||||
placeholder="URL"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
defaultValue={props.user.avatar}
|
||||
/>
|
||||
</div>
|
||||
</div>*/}
|
||||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="py-4 flex justify-end">
|
||||
<Button type="submit">{t("save")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => <SettingsView {...props} localeProp={data.locale} />}
|
||||
/>
|
||||
</SettingsShell>
|
||||
</Shell>
|
||||
);
|
||||
|
@ -420,7 +405,6 @@ export default function Settings(props: Props) {
|
|||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getSession(context);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
@ -451,10 +435,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
localeProp: locale,
|
||||
localeOptions,
|
||||
localeLabels,
|
||||
user: {
|
||||
...user,
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
|
|
Loading…
Reference in New Issue