cal.pub0.org/pages/settings/profile.tsx

318 lines
14 KiB
TypeScript

import { GetServerSideProps } from "next";
import Head from "next/head";
import { useEffect, useRef, useState } from "react";
import prisma, { whereAndSelect } from "@lib/prisma";
import Modal from "../../components/Modal";
import Shell from "../../components/Shell";
import SettingsShell from "../../components/Settings";
import Avatar from "../../components/Avatar";
import { getSession } from "next-auth/client";
import Select from "react-select";
import TimezoneSelect from "react-timezone-select";
import { UsernameInput } from "../../components/ui/UsernameInput";
import ErrorAlert from "../../components/ui/alerts/Error";
const themeOptions = [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
];
export default function Settings(props) {
const [successModalOpen, setSuccessModalOpen] = useState(false);
const usernameRef = useRef<HTMLInputElement>();
const nameRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const avatarRef = useRef<HTMLInputElement>();
const hideBrandingRef = useRef<HTMLInputElement>();
const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme });
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart });
const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
useEffect(() => {
setSelectedTheme(
props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : null
);
setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart });
}, []);
const closeSuccessModal = () => {
setSuccessModalOpen(false);
};
const handleError = async (resp) => {
if (!resp.ok) {
const error = await resp.json();
throw new Error(error.message);
}
};
async function updateProfileHandler(event) {
event.preventDefault();
const enteredUsername = usernameRef.current.value.toLowerCase();
const enteredName = nameRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredAvatar = avatarRef.current.value;
const enteredTimeZone = selectedTimeZone.value;
const enteredWeekStartDay = selectedWeekStartDay.value;
const enteredHideBranding = hideBrandingRef.current.checked;
// TODO: Add validation
await fetch("/api/user/profile", {
method: "PATCH",
body: JSON.stringify({
username: enteredUsername,
name: enteredName,
description: enteredDescription,
avatar: enteredAvatar,
timeZone: enteredTimeZone,
weekStart: enteredWeekStartDay,
hideBranding: enteredHideBranding,
theme: selectedTheme ? selectedTheme.value : null,
}),
headers: {
"Content-Type": "application/json",
},
})
.then(handleError)
.then(() => {
setSuccessModalOpen(true);
setHasErrors(false); // dismiss any open errors
})
.catch((err) => {
setHasErrors(true);
setErrorMessage(err.message);
});
}
return (
<Shell heading="Profile" subtitle="Edit your profile information, which shows on your scheduling link.">
<Head>
<title>Profile | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<SettingsShell>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}>
{hasErrors && <ErrorAlert message={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">
Full name
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
autoComplete="given-name"
placeholder="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>
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
About
</label>
<div className="mt-1">
<textarea
ref={descriptionRef}
id="about"
name="about"
placeholder="A little something about yourself."
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>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
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">
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: "Sunday" },
{ value: "Monday", label: "Monday" },
]}
/>
</div>
</div>
<div>
<label htmlFor="theme" className="block text-sm font-medium text-gray-700">
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 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])}
defaultChecked={!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">
Automatically adjust theme based on invitee preferences
</label>
</div>
</div>
</div>
<div>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
id="hide-branding"
name="hide-branding"
type="checkbox"
ref={hideBrandingRef}
defaultChecked={props.user.hideBranding}
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="hide-branding" className="font-medium text-gray-700">
Disable Calendso branding
</label>
<p className="text-gray-500">Hide all Calendso branding from your public pages.</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 className="ml-5 rounded-sm shadow-sm">
<div className="group relative border border-gray-300 rounded-sm py-2 px-3 flex items-center justify-center hover:bg-gray-50 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-neutral-500">
<label htmlFor="user_photo" className="relative text-sm leading-4 font-medium text-gray-700 pointer-events-none">
<span>Change</span>
<span className="sr-only"> user photo</span>
</label>
<input id="user_photo" name="user_photo" type="file" className="absolute w-full h-full opacity-0 cursor-pointer border-gray-300 rounded-sm" />
</div>
</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>}
/>
{/* <label htmlFor="user-photo" className="absolute inset-0 w-full h-full bg-black bg-opacity-75 flex items-center justify-center text-sm font-medium text-white opacity-0 hover:opacity-100 focus-within:opacity-100">
<span>Change</span>
<span className="sr-only"> user photo</span>
<input type="file" id="user-photo" name="user-photo" className="absolute inset-0 w-full h-full opacity-0 cursor-pointer border-gray-300 rounded-sm" />
</label> */}
</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"
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500">
Save
</button>
</div>
</div>
</form>
<Modal
heading="Profile updated successfully"
description="Your user profile has been updated successfully."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
</SettingsShell>
</Shell>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await whereAndSelect(
prisma.user.findFirst,
{
id: session.user.id,
},
["id", "username", "name", "email", "bio", "avatar", "timeZone", "weekStart", "hideBranding", "theme"]
);
return {
props: { user }, // will be passed to the page component as props
};
};