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>
This commit is contained in:
parent
b5e176a87e
commit
ce8e9c126b
3 changed files with 279 additions and 319 deletions
|
@ -44,31 +44,3 @@ export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promi
|
||||||
|
|
||||||
return preferredLocale;
|
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] };
|
|
||||||
});
|
|
||||||
|
|
8
lib/core/i18n/weekday.ts
Normal file
8
lib/core/i18n/weekday.ts
Normal file
|
@ -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 { InformationCircleIcon } from "@heroicons/react/outline";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { RefObject, useEffect, useRef, useState } from "react";
|
import { i18n } from "next-i18next.config";
|
||||||
import Select from "react-select";
|
import { ComponentProps, RefObject, useEffect, useRef, useState } from "react";
|
||||||
|
import Select, { OptionTypeBase } from "react-select";
|
||||||
import TimezoneSelect from "react-timezone-select";
|
import TimezoneSelect from "react-timezone-select";
|
||||||
|
|
||||||
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import {
|
import { nameOfDay } from "@lib/core/i18n/weekday";
|
||||||
getOrSetUserLocaleFromHeaders,
|
|
||||||
localeLabels,
|
|
||||||
localeOptions,
|
|
||||||
OptionType,
|
|
||||||
} from "@lib/core/i18n/i18n.utils";
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import showToast from "@lib/notification";
|
import showToast from "@lib/notification";
|
||||||
|
@ -20,7 +17,7 @@ import prisma from "@lib/prisma";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
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 ImageUploader from "@components/ImageUploader";
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
|
@ -31,6 +28,14 @@ import Button from "@components/ui/Button";
|
||||||
import { UsernameInput } from "@components/ui/UsernameInput";
|
import { UsernameInput } from "@components/ui/UsernameInput";
|
||||||
|
|
||||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
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"] }) {
|
function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>; user: Props["user"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [modelOpen, setModalOpen] = useState(false);
|
const [modelOpen, setModalOpen] = useState(false);
|
||||||
|
@ -58,12 +63,12 @@ function HideBrandingInput(props: { hideBrandingRef: RefObject<HTMLInputElement>
|
||||||
/>
|
/>
|
||||||
<Dialog open={modelOpen}>
|
<Dialog open={modelOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100 mb-4">
|
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-yellow-100 rounded-full">
|
||||||
<InformationCircleIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />
|
<InformationCircleIcon className="w-6 h-6 text-yellow-400" aria-hidden="true" />
|
||||||
</div>
|
</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">
|
<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")}
|
{t("only_available_on_pro_plan")}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</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">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-x-2">
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button
|
||||||
className="btn-wide btn-primary text-center table-cell"
|
className="table-cell text-center btn-wide btn-primary"
|
||||||
onClick={() => setModalOpen(false)}>
|
onClick={() => setModalOpen(false)}>
|
||||||
{t("dismiss")}
|
{t("dismiss")}
|
||||||
</Button>
|
</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 { 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 = [
|
const themeOptions = [
|
||||||
{ value: "light", label: t("light") },
|
{ value: "light", label: t("light") },
|
||||||
|
@ -109,15 +130,16 @@ export default function Settings(props: Props) {
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>(null!);
|
const descriptionRef = useRef<HTMLTextAreaElement>(null!);
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||||
const hideBrandingRef = 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 [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
|
||||||
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({
|
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState<OptionTypeBase>({
|
||||||
value: props.user.weekStart,
|
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,
|
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 [imageSrc, setImageSrc] = useState<string>(props.user.avatar || "");
|
||||||
const [hasErrors, setHasErrors] = useState(false);
|
const [hasErrors, setHasErrors] = useState(false);
|
||||||
|
@ -127,8 +149,6 @@ export default function Settings(props: Props) {
|
||||||
setSelectedTheme(
|
setSelectedTheme(
|
||||||
props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : undefined
|
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) {
|
async function updateProfileHandler(event) {
|
||||||
|
@ -145,8 +165,7 @@ export default function Settings(props: Props) {
|
||||||
|
|
||||||
// TODO: Add validation
|
// TODO: Add validation
|
||||||
|
|
||||||
await mutation
|
mutation.mutate({
|
||||||
.mutateAsync({
|
|
||||||
username: enteredUsername,
|
username: enteredUsername,
|
||||||
name: enteredName,
|
name: enteredName,
|
||||||
bio: enteredDescription,
|
bio: enteredDescription,
|
||||||
|
@ -156,28 +175,17 @@ export default function Settings(props: Props) {
|
||||||
hideBranding: enteredHideBranding,
|
hideBranding: enteredHideBranding,
|
||||||
theme: asStringOrNull(selectedTheme?.value),
|
theme: asStringOrNull(selectedTheme?.value),
|
||||||
locale: enteredLanguage,
|
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" });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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}>
|
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}>
|
||||||
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
||||||
<div className="py-6 lg:pb-8">
|
<div className="py-6 lg:pb-8">
|
||||||
<div className="flex flex-col lg:flex-row">
|
<div className="flex flex-col lg:flex-row">
|
||||||
<div className="flex-grow space-y-6">
|
<div className="flex-grow space-y-6">
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
<div className="w-full sm:w-1/2 sm:mr-2 mb-6">
|
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
|
||||||
<UsernameInput ref={usernameRef} defaultValue={props.user.username} />
|
<UsernameInput ref={usernameRef} defaultValue={props.user.username} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2 sm:ml-2">
|
<div className="w-full sm:w-1/2 sm:ml-2">
|
||||||
|
@ -192,14 +200,14 @@ export default function Settings(props: Props) {
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
placeholder={t("your_name")}
|
placeholder={t("your_name")}
|
||||||
required
|
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"
|
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}
|
defaultValue={props.user.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
<div className="w-full sm:w-1/2 sm:mr-2 mb-6">
|
<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">
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
{t("email")}
|
{t("email")}
|
||||||
</label>
|
</label>
|
||||||
|
@ -209,7 +217,7 @@ export default function Settings(props: Props) {
|
||||||
id="email"
|
id="email"
|
||||||
placeholder={t("your_email")}
|
placeholder={t("your_email")}
|
||||||
disabled
|
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"
|
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}
|
defaultValue={props.user.email}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||||
|
@ -232,15 +240,15 @@ export default function Settings(props: Props) {
|
||||||
name="about"
|
name="about"
|
||||||
placeholder={t("little_something_about")}
|
placeholder={t("little_something_about")}
|
||||||
rows={3}
|
rows={3}
|
||||||
defaultValue={props.user.bio}
|
defaultValue={props.user.bio || undefined}
|
||||||
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>
|
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>
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-1 flex">
|
<div className="flex mt-1">
|
||||||
<Avatar
|
<Avatar
|
||||||
displayName={props.user.name}
|
displayName={props.user.name}
|
||||||
className="relative rounded-full w-10 h-10"
|
className="relative w-10 h-10 rounded-full"
|
||||||
gravatarFallbackMd5={props.user.emailMd5}
|
gravatarFallbackMd5={props.user.emailMd5}
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
/>
|
/>
|
||||||
|
@ -250,7 +258,7 @@ export default function Settings(props: Props) {
|
||||||
name="avatar"
|
name="avatar"
|
||||||
id="avatar"
|
id="avatar"
|
||||||
placeholder="URL"
|
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"
|
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}
|
defaultValue={imageSrc}
|
||||||
/>
|
/>
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
|
@ -284,8 +292,8 @@ export default function Settings(props: Props) {
|
||||||
value={selectedLanguage || props.localeProp}
|
value={selectedLanguage || props.localeProp}
|
||||||
onChange={setSelectedLanguage}
|
onChange={setSelectedLanguage}
|
||||||
classNamePrefix="react-select"
|
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"
|
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={props.localeOptions}
|
options={localeOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -299,7 +307,7 @@ export default function Settings(props: Props) {
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={setSelectedTimeZone}
|
onChange={setSelectedTimeZone}
|
||||||
classNamePrefix="react-select"
|
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"
|
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>
|
</div>
|
||||||
|
@ -313,10 +321,10 @@ export default function Settings(props: Props) {
|
||||||
value={selectedWeekStartDay}
|
value={selectedWeekStartDay}
|
||||||
onChange={setSelectedWeekStartDay}
|
onChange={setSelectedWeekStartDay}
|
||||||
classNamePrefix="react-select"
|
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"
|
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={[
|
options={[
|
||||||
{ value: "Sunday", label: t("sunday") },
|
{ value: "Sunday", label: nameOfDay(props.localeProp, 0) },
|
||||||
{ value: "Monday", label: t("monday") },
|
{ value: "Monday", label: nameOfDay(props.localeProp, 1) },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -336,7 +344,7 @@ export default function Settings(props: Props) {
|
||||||
options={themeOptions}
|
options={themeOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 relative flex items-start">
|
<div className="relative flex items-start mt-8">
|
||||||
<div className="flex items-center h-5">
|
<div className="flex items-center h-5">
|
||||||
<input
|
<input
|
||||||
id="theme-adjust-os"
|
id="theme-adjust-os"
|
||||||
|
@ -344,7 +352,7 @@ export default function Settings(props: Props) {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
|
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
|
||||||
checked={!selectedTheme}
|
checked={!selectedTheme}
|
||||||
className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm"
|
className="w-4 h-4 border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-sm">
|
<div className="ml-3 text-sm">
|
||||||
|
@ -369,50 +377,27 @@ export default function Settings(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<hr className="mt-8" />
|
<hr className="mt-8" />
|
||||||
<div className="py-4 flex justify-end">
|
<div className="flex justify-end py-4">
|
||||||
<Button type="submit">{t("save")}</Button>
|
<Button type="submit">{t("save")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => <SettingsView {...props} localeProp={data.locale} />}
|
||||||
|
/>
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
|
@ -420,7 +405,6 @@ export default function Settings(props: Props) {
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
|
@ -451,10 +435,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
session,
|
|
||||||
localeProp: locale,
|
|
||||||
localeOptions,
|
|
||||||
localeLabels,
|
|
||||||
user: {
|
user: {
|
||||||
...user,
|
...user,
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||||
|
|
Loading…
Reference in a new issue