Prevent users from entering mixed case usernames
Booking pages are case insensitive new, so no more case sensitive usernames.
This commit is contained in:
parent
afa2e19f03
commit
1668785678
3 changed files with 496 additions and 318 deletions
|
@ -1,152 +1,268 @@
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {signOut, useSession} from 'next-auth/client';
|
import { signOut, useSession } from "next-auth/client";
|
||||||
import {MenuIcon, XIcon} from '@heroicons/react/outline';
|
import { MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||||
import {collectPageParameters, telemetryEventTypes, useTelemetry} from "../lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../lib/telemetry";
|
||||||
|
|
||||||
export default function Shell(props) {
|
export default function Shell(props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [ session, loading ] = useSession();
|
const [session, loading] = useSession();
|
||||||
const [ profileDropdownExpanded, setProfileDropdownExpanded ] = useState(false);
|
const [profileDropdownExpanded, setProfileDropdownExpanded] = useState(false);
|
||||||
const [ mobileMenuExpanded, setMobileMenuExpanded ] = useState(false);
|
const [mobileMenuExpanded, setMobileMenuExpanded] = useState(false);
|
||||||
let telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
telemetry.withJitsu((jitsu) => {
|
telemetry.withJitsu((jitsu) => {
|
||||||
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.pathname))
|
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.pathname));
|
||||||
});
|
});
|
||||||
}, [telemetry])
|
}, [telemetry]);
|
||||||
|
|
||||||
const toggleProfileDropdown = () => {
|
const toggleProfileDropdown = () => {
|
||||||
setProfileDropdownExpanded(!profileDropdownExpanded);
|
setProfileDropdownExpanded(!profileDropdownExpanded);
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
setMobileMenuExpanded(!mobileMenuExpanded);
|
setMobileMenuExpanded(!mobileMenuExpanded);
|
||||||
}
|
};
|
||||||
|
|
||||||
const logoutHandler = () => {
|
const logoutHandler = () => {
|
||||||
signOut({ redirect: false }).then( () => router.push('/auth/logout') );
|
signOut({ redirect: false }).then(() => router.push("/auth/logout"));
|
||||||
}
|
};
|
||||||
|
|
||||||
if ( ! loading && ! session ) {
|
if (!loading && !session) {
|
||||||
router.replace('/auth/login');
|
router.replace("/auth/login");
|
||||||
}
|
} else if (loading) {
|
||||||
|
return <p className="text-gray-400">Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
return session && (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-gradient-to-b from-blue-600 via-blue-600 to-blue-300 pb-32">
|
<div className="bg-gradient-to-b from-blue-600 via-blue-600 to-blue-300 pb-32">
|
||||||
<nav className="bg-blue-600">
|
<nav className="bg-blue-600">
|
||||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div className="border-b border-blue-500">
|
<div className="border-b border-blue-500">
|
||||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<img className="h-6" src="/calendso-white.svg" alt="Calendso" />
|
<img className="h-6" src="/calendso-white.svg" alt="Calendso" />
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<div className="ml-10 flex items-baseline space-x-4">
|
<div className="ml-10 flex items-baseline space-x-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className={router.pathname == "/" ? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium" : "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Dashboard</a>
|
<a
|
||||||
</Link>
|
className={
|
||||||
{/* <Link href="/">
|
router.pathname == "/"
|
||||||
|
? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
: "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
}>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
{/* <Link href="/">
|
||||||
<a className={router.pathname.startsWith("/bookings") ? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium" : "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Bookings</a>
|
<a className={router.pathname.startsWith("/bookings") ? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium" : "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Bookings</a>
|
||||||
</Link> */}
|
</Link> */}
|
||||||
<Link href="/availability">
|
<Link href="/availability">
|
||||||
<a className={router.pathname.startsWith("/availability") ? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium" : "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Availability</a>
|
<a
|
||||||
</Link>
|
className={
|
||||||
<Link href="/integrations">
|
router.pathname.startsWith("/availability")
|
||||||
<a className={router.pathname.startsWith("/integrations") ? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium" : "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Integrations</a>
|
? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
</Link>
|
: "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
<Link href="/settings/profile">
|
}>
|
||||||
<a className={router.pathname.startsWith("/settings") ? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium" : "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"}>Settings</a>
|
Availability
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
<Link href="/integrations">
|
||||||
</div>
|
<a
|
||||||
<div className="hidden md:block">
|
className={
|
||||||
<div className="ml-4 flex items-center md:ml-6">
|
router.pathname.startsWith("/integrations")
|
||||||
<div className="ml-3 relative">
|
? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
<div>
|
: "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
<button onClick={toggleProfileDropdown} type="button" className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" id="user-menu" aria-expanded="false" aria-haspopup="true">
|
}>
|
||||||
<span className="sr-only">Open user menu</span>
|
Integrations
|
||||||
<img className="h-8 w-8 rounded-full" src={session.user.image ? session.user.image : "https://eu.ui-avatars.com/api/?background=fff&color=039be5&name=" + encodeURIComponent(session.user.name || "")} alt="" />
|
</a>
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
<Link href="/settings/profile">
|
||||||
{
|
<a
|
||||||
profileDropdownExpanded && (
|
className={
|
||||||
<div className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50" role="menu" aria-orientation="vertical" aria-labelledby="user-menu">
|
router.pathname.startsWith("/settings")
|
||||||
<Link href={"/" + session.user.username}><a target="_blank" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">Your Public Page</a></Link>
|
? "bg-blue-500 transition-colors duration-300 ease-in-out text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
<Link href="/settings/profile"><a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">Your Profile</a></Link>
|
: "text-white hover:bg-blue-500 transition-colors duration-300 ease-in-out hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
<Link href="/settings/password"><a className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">Login & Security</a></Link>
|
}>
|
||||||
<button onClick={logoutHandler} className="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem">Sign out</button>
|
Settings
|
||||||
</div>
|
</a>
|
||||||
)
|
</Link>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="-mr-2 flex md:hidden">
|
|
||||||
<button onClick={toggleMobileMenu} type="button" className=" inline-flex items-center justify-center p-2 rounded-md text-white focus:outline-none" aria-controls="mobile-menu" aria-expanded="false">
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
{ !mobileMenuExpanded && <MenuIcon className="block h-6 w-6" /> }
|
|
||||||
{ mobileMenuExpanded && <XIcon className="block h-6 w-6" /> }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{ mobileMenuExpanded && <div className="border-b border-blue-500 md:hidden bg-blue-600" id="mobile-menu">
|
|
||||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
|
||||||
<Link href="/">
|
|
||||||
<a className={router.pathname == "/" ? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Dashboard</a>
|
|
||||||
</Link>
|
|
||||||
<Link href="/availability">
|
|
||||||
<a className={router.pathname.startsWith("/availability") ? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Availability</a>
|
|
||||||
</Link>
|
|
||||||
<Link href="/integrations">
|
|
||||||
<a className={router.pathname.startsWith("/integrations") ? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Integrations</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="pt-4 pb-3 border-t border-blue-500">
|
|
||||||
<div className="flex items-center px-5">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<img className="h-10 w-10 rounded-full" src={"https://eu.ui-avatars.com/api/?background=039be5&color=fff&name=" + encodeURIComponent(session.user.name || session.user.username)} alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<div className="text-base font-medium leading-none text-white">{session.user.name || session.user.username}</div>
|
|
||||||
<div className="text-sm font-medium leading-none text-gray-200">{session.user.email}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 px-2 space-y-1">
|
|
||||||
<Link href="/settings/profile">
|
|
||||||
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-white hover:bg-gray-700">Your Profile</a>
|
|
||||||
</Link>
|
|
||||||
<Link href="/settings">
|
|
||||||
<a className={router.pathname.startsWith("/settings") ? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium" : "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"}>Settings</a>
|
|
||||||
</Link>
|
|
||||||
<button onClick={logoutHandler} className="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-white hover:bg-gray-700">Sign out</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</nav>
|
|
||||||
<header className={props.noPaddingBottom ? "pt-10" : "py-10"}>
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<h1 className="text-3xl font-bold text-white">
|
|
||||||
{props.heading}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main className="-mt-32">
|
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
{props.children}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<div className="hidden md:block">
|
||||||
</div>
|
<div className="ml-4 flex items-center md:ml-6">
|
||||||
);
|
<div className="ml-3 relative">
|
||||||
}
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={toggleProfileDropdown}
|
||||||
|
type="button"
|
||||||
|
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||||
|
id="user-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true">
|
||||||
|
<span className="sr-only">Open user menu</span>
|
||||||
|
<img
|
||||||
|
className="h-8 w-8 rounded-full"
|
||||||
|
src={
|
||||||
|
session.user.image
|
||||||
|
? session.user.image
|
||||||
|
: "https://eu.ui-avatars.com/api/?background=fff&color=039be5&name=" +
|
||||||
|
encodeURIComponent(session.user.name || "")
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{profileDropdownExpanded && (
|
||||||
|
<div
|
||||||
|
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50"
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-labelledby="user-menu">
|
||||||
|
<Link href={"/" + session.user.username}>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
role="menuitem">
|
||||||
|
Your Public Page
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings/profile">
|
||||||
|
<a
|
||||||
|
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
role="menuitem">
|
||||||
|
Your Profile
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings/password">
|
||||||
|
<a
|
||||||
|
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
role="menuitem">
|
||||||
|
Login & Security
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={logoutHandler}
|
||||||
|
className="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
role="menuitem">
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mr-2 flex md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
type="button"
|
||||||
|
className=" inline-flex items-center justify-center p-2 rounded-md text-white focus:outline-none"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span className="sr-only">Open main menu</span>
|
||||||
|
{!mobileMenuExpanded && <MenuIcon className="block h-6 w-6" />}
|
||||||
|
{mobileMenuExpanded && <XIcon className="block h-6 w-6" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mobileMenuExpanded && (
|
||||||
|
<div className="border-b border-blue-500 md:hidden bg-blue-600" id="mobile-menu">
|
||||||
|
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||||
|
<Link href="/">
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
router.pathname == "/"
|
||||||
|
? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
: "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
}>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/availability">
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
router.pathname.startsWith("/availability")
|
||||||
|
? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
: "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
}>
|
||||||
|
Availability
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/integrations">
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
router.pathname.startsWith("/integrations")
|
||||||
|
? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
: "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
}>
|
||||||
|
Integrations
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 pb-3 border-t border-blue-500">
|
||||||
|
<div className="flex items-center px-5">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<img
|
||||||
|
className="h-10 w-10 rounded-full"
|
||||||
|
src={
|
||||||
|
"https://eu.ui-avatars.com/api/?background=039be5&color=fff&name=" +
|
||||||
|
encodeURIComponent(session.user.name || session.user.username)
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<div className="text-base font-medium leading-none text-white">
|
||||||
|
{session.user.name || session.user.username}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium leading-none text-gray-200">{session.user.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 px-2 space-y-1">
|
||||||
|
<Link href="/settings/profile">
|
||||||
|
<a className="block px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-white hover:bg-gray-700">
|
||||||
|
Your Profile
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings">
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
router.pathname.startsWith("/settings")
|
||||||
|
? "bg-blue-500 text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
: "text-gray-100 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
}>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={logoutHandler}
|
||||||
|
className="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-gray-100 hover:text-white hover:bg-gray-700">
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
<header className={props.noPaddingBottom ? "pt-10" : "py-10"}>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 className="text-3xl font-bold text-white">{props.heading}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="-mt-32">
|
||||||
|
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">{props.children}</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const UsernameInput = React.forwardRef( (props, ref) => (
|
const UsernameInput = React.forwardRef((props, ref) => (
|
||||||
// todo, check if username is already taken here?
|
// todo, check if username is already taken here?
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||||
|
@ -10,8 +10,20 @@ export const UsernameInput = React.forwardRef( (props, ref) => (
|
||||||
<span className="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm">
|
<span className="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm">
|
||||||
{typeof window !== "undefined" && window.location.hostname}/
|
{typeof window !== "undefined" && window.location.hostname}/
|
||||||
</span>
|
</span>
|
||||||
<input ref={ref} type="text" name="username" id="username" autoComplete="username" required {...props}
|
<input
|
||||||
className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"/>
|
ref={ref}
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
autoComplete="username"
|
||||||
|
required
|
||||||
|
{...props}
|
||||||
|
className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300 lowercase"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
UsernameInput.displayName = "UsernameInput";
|
||||||
|
|
||||||
|
export { UsernameInput };
|
||||||
|
|
|
@ -1,140 +1,173 @@
|
||||||
import Head from 'next/head';
|
import { GetServerSideProps } from "next";
|
||||||
import Link from 'next/link';
|
import Head from "next/head";
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from "react";
|
||||||
import { useRouter } from 'next/router';
|
import prisma from "../../lib/prisma";
|
||||||
import prisma from '../../lib/prisma';
|
import Modal from "../../components/Modal";
|
||||||
import Modal from '../../components/Modal';
|
import Shell from "../../components/Shell";
|
||||||
import Shell from '../../components/Shell';
|
import SettingsShell from "../../components/Settings";
|
||||||
import SettingsShell from '../../components/Settings';
|
import Avatar from "../../components/Avatar";
|
||||||
import Avatar from '../../components/Avatar';
|
import { getSession } from "next-auth/client";
|
||||||
import { signIn, useSession, getSession } from 'next-auth/client';
|
import TimezoneSelect from "react-timezone-select";
|
||||||
import TimezoneSelect from 'react-timezone-select';
|
import { UsernameInput } from "../../components/ui/UsernameInput";
|
||||||
import {UsernameInput} from "../../components/ui/UsernameInput";
|
|
||||||
import ErrorAlert from "../../components/ui/alerts/Error";
|
import ErrorAlert from "../../components/ui/alerts/Error";
|
||||||
|
|
||||||
export default function Settings(props) {
|
export default function Settings(props) {
|
||||||
const [ session, loading ] = useSession();
|
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||||
const router = useRouter();
|
const usernameRef = useRef<HTMLInputElement>();
|
||||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
const nameRef = useRef<HTMLInputElement>();
|
||||||
const usernameRef = useRef<HTMLInputElement>();
|
const descriptionRef = useRef<HTMLTextAreaElement>();
|
||||||
const nameRef = useRef<HTMLInputElement>();
|
const avatarRef = useRef<HTMLInputElement>();
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>();
|
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
|
||||||
const avatarRef = useRef<HTMLInputElement>();
|
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState(props.user.weekStart || "Sunday");
|
||||||
|
|
||||||
const [ selectedTimeZone, setSelectedTimeZone ] = useState({ value: props.user.timeZone });
|
const [hasErrors, setHasErrors] = useState(false);
|
||||||
const [ selectedWeekStartDay, setSelectedWeekStartDay ] = useState(props.user.weekStart || 'Sunday');
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
const [ hasErrors, setHasErrors ] = useState(false);
|
const closeSuccessModal = () => {
|
||||||
const [ errorMessage, setErrorMessage ] = useState('');
|
setSuccessModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
const handleError = async (resp) => {
|
||||||
return <p className="text-gray-400">Loading...</p>;
|
if (!resp.ok) {
|
||||||
|
const error = await resp.json();
|
||||||
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const closeSuccessModal = () => { setSuccessModalOpen(false); }
|
async function updateProfileHandler(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const handleError = async (resp) => {
|
const enteredUsername = usernameRef.current.value.toLowerCase();
|
||||||
if (!resp.ok) {
|
const enteredName = nameRef.current.value;
|
||||||
const error = await resp.json();
|
const enteredDescription = descriptionRef.current.value;
|
||||||
throw new Error(error.message);
|
const enteredAvatar = avatarRef.current.value;
|
||||||
}
|
const enteredTimeZone = selectedTimeZone.value;
|
||||||
}
|
const enteredWeekStartDay = selectedWeekStartDay;
|
||||||
|
|
||||||
async function updateProfileHandler(event) {
|
// TODO: Add validation
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const enteredUsername = usernameRef.current.value;
|
await fetch("/api/user/profile", {
|
||||||
const enteredName = nameRef.current.value;
|
method: "PATCH",
|
||||||
const enteredDescription = descriptionRef.current.value;
|
body: JSON.stringify({
|
||||||
const enteredAvatar = avatarRef.current.value;
|
username: enteredUsername,
|
||||||
const enteredTimeZone = selectedTimeZone.value;
|
name: enteredName,
|
||||||
const enteredWeekStartDay = selectedWeekStartDay;
|
description: enteredDescription,
|
||||||
|
avatar: enteredAvatar,
|
||||||
|
timeZone: enteredTimeZone,
|
||||||
|
weekStart: enteredWeekStartDay,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(handleError)
|
||||||
|
.then(() => {
|
||||||
|
setSuccessModalOpen(true);
|
||||||
|
setHasErrors(false); // dismiss any open errors
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setHasErrors(true);
|
||||||
|
setErrorMessage(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Add validation
|
return (
|
||||||
|
<Shell heading="Profile">
|
||||||
|
<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 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg leading-6 font-medium text-gray-900">Profile</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Review and change your public page details.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
const response = await fetch('/api/user/profile', {
|
<div className="mt-6 flex flex-col lg:flex-row">
|
||||||
method: 'PATCH',
|
<div className="flex-grow space-y-6">
|
||||||
body: JSON.stringify({username: enteredUsername, name: enteredName, description: enteredDescription, avatar: enteredAvatar, timeZone: enteredTimeZone, weekStart: enteredWeekStartDay}),
|
<div className="flex">
|
||||||
headers: {
|
<div className="w-1/2 mr-2">
|
||||||
'Content-Type': 'application/json'
|
<UsernameInput ref={usernameRef} defaultValue={props.user.username} />
|
||||||
}
|
</div>
|
||||||
}).then(handleError).then( () => {
|
<div className="w-1/2 ml-2">
|
||||||
setSuccessModalOpen(true);
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
setHasErrors(false); // dismiss any open errors
|
Full name
|
||||||
}).catch( (err) => {
|
</label>
|
||||||
setHasErrors(true);
|
<input
|
||||||
setErrorMessage(err.message);
|
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-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
defaultValue={props.user.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return(
|
<div>
|
||||||
<Shell heading="Profile">
|
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
||||||
<Head>
|
About
|
||||||
<title>Profile | Calendso</title>
|
</label>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<div className="mt-1">
|
||||||
</Head>
|
<textarea
|
||||||
<SettingsShell>
|
ref={descriptionRef}
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}>
|
id="about"
|
||||||
{hasErrors && <ErrorAlert message={errorMessage} />}
|
name="about"
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
placeholder="A little something about yourself."
|
||||||
<div>
|
rows={3}
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900">Profile</h2>
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
{props.user.bio}
|
||||||
Review and change your public page details.
|
</textarea>
|
||||||
</p>
|
</div>
|
||||||
</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}
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</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={(e) => setSelectedWeekStartDay(e.target.value)}
|
||||||
|
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
|
||||||
|
<option value="Sunday">Sunday</option>
|
||||||
|
<option value="Monday">Monday</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex flex-col lg:flex-row">
|
<div className="mt-6 flex-grow lg:mt-0 lg:ml-6 lg:flex-grow-0 lg:flex-shrink-0">
|
||||||
<div className="flex-grow space-y-6">
|
<p className="mb-2 text-sm font-medium text-gray-700" aria-hidden="true">
|
||||||
<div className="flex">
|
Photo
|
||||||
<div className="w-1/2 mr-2">
|
</p>
|
||||||
<UsernameInput ref={usernameRef} defaultValue={props.user.username} />
|
<div className="mt-1 lg:hidden">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
<div className="w-1/2 ml-2">
|
<div
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Full name</label>
|
className="flex-shrink-0 inline-block rounded-full overflow-hidden h-12 w-12"
|
||||||
<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-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" defaultValue={props.user.name} />
|
aria-hidden="true">
|
||||||
</div>
|
<Avatar user={props.user} className="rounded-full h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div className="ml-5 rounded-md shadow-sm">
|
||||||
<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} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">{props.user.bio}</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} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" />
|
|
||||||
</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={e => setSelectedWeekStartDay(e.target.value)} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
|
|
||||||
<option value="Sunday">Sunday</option>
|
|
||||||
<option value="Monday">Monday</option>
|
|
||||||
</select>
|
|
||||||
</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-md shadow-sm">
|
|
||||||
<div className="group relative border border-gray-300 rounded-md 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-blue-500">
|
<div className="group relative border border-gray-300 rounded-md 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-blue-500">
|
||||||
<label htmlFor="user_photo" className="relative text-sm leading-4 font-medium text-gray-700 pointer-events-none">
|
<label htmlFor="user_photo" className="relative text-sm leading-4 font-medium text-gray-700 pointer-events-none">
|
||||||
<span>Change</span>
|
<span>Change</span>
|
||||||
|
@ -143,64 +176,81 @@ export default function Settings(props) {
|
||||||
<input id="user_photo" name="user_photo" type="file" className="absolute w-full h-full opacity-0 cursor-pointer border-gray-300 rounded-md" />
|
<input id="user_photo" name="user_photo" type="file" className="absolute w-full h-full opacity-0 cursor-pointer border-gray-300 rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden relative rounded-full overflow-hidden lg:block">
|
<div className="hidden relative rounded-full overflow-hidden lg:block">
|
||||||
<Avatar
|
<Avatar
|
||||||
user={props.user}
|
user={props.user}
|
||||||
className="relative rounded-full w-40 h-40"
|
className="relative rounded-full w-40 h-40"
|
||||||
fallback={<div className="relative bg-blue-600 rounded-full w-40 h-40"></div>}
|
fallback={<div className="relative bg-blue-600 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">
|
{/* <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>Change</span>
|
||||||
<span className="sr-only"> user photo</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-md" />
|
<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-md" />
|
||||||
</label> */}
|
</label> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label htmlFor="avatar" className="block text-sm font-medium text-gray-700">Avatar URL</label>
|
<label htmlFor="avatar" className="block text-sm font-medium text-gray-700">
|
||||||
<input ref={avatarRef} type="text" name="avatar" id="avatar" placeholder="URL" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" defaultValue={props.user.avatar} />
|
Avatar URL
|
||||||
</div>
|
</label>
|
||||||
</div>
|
<input
|
||||||
</div>
|
ref={avatarRef}
|
||||||
<hr className="mt-8" />
|
type="text"
|
||||||
<div className="py-4 flex justify-end">
|
name="avatar"
|
||||||
<button type="submit" className="ml-2 bg-blue-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
id="avatar"
|
||||||
Save
|
placeholder="URL"
|
||||||
</button>
|
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
</div>
|
defaultValue={props.user.avatar}
|
||||||
</div>
|
/>
|
||||||
</form>
|
</div>
|
||||||
<Modal heading="Profile updated successfully" description="Your user profile has been updated successfully." open={successModalOpen} handleClose={closeSuccessModal} />
|
</div>
|
||||||
</SettingsShell>
|
</div>
|
||||||
</Shell>
|
<hr className="mt-8" />
|
||||||
);
|
<div className="py-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="ml-2 bg-blue-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-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 async function getServerSideProps(context) {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return { redirect: { permanent: false, destination: '/auth/login' } };
|
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
bio: true,
|
bio: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
weekStart: true,
|
weekStart: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user}, // will be passed to the page component as props
|
props: { user }, // will be passed to the page component as props
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
Loading…
Reference in a new issue