Initial restyling

This commit is contained in:
Bailey Pumfleet 2021-07-31 00:05:38 +01:00
parent 062b92be29
commit a608f94590
24 changed files with 3566 additions and 1585 deletions

View File

@ -1,87 +1,71 @@
import ActiveLink from '../components/ActiveLink';
import {CodeIcon, CreditCardIcon, KeyIcon, UserCircleIcon, UserGroupIcon} from '@heroicons/react/outline';
import Link from 'next/link';
import { CreditCardIcon, UserIcon, CodeIcon, KeyIcon, UserGroupIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
export default function SettingsShell(props) {
return (
<div>
<main className="relative -mt-32">
<div>
<div className="bg-white rounded-lg shadow">
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside className="py-6 lg:col-span-3">
<nav id="nav--settings" className="space-y-1">
const router = useRouter();
<ActiveLink href="/settings/profile">
<a><UserCircleIcon /> Profile</a>
</ActiveLink>
const tabs = [
{ name: "Profile", href: "/settings/profile", icon: UserIcon, current: router.pathname == "/settings/profile" },
{ name: "Password", href: "/settings/password", icon: KeyIcon, current: router.pathname == "/settings/password" },
{ name: "Embed", href: "/settings/embed", icon: CodeIcon, current: router.pathname == "/settings/embed" },
{ name: "Teams", href: "/settings/teams", icon: UserGroupIcon, current: router.pathname == "/settings/teams" },
{ name: "Billing", href: "/settings/billing", icon: CreditCardIcon, current: router.pathname == "/settings/billing" },
];
{/* <Link href="/settings/account">
<a className={router.pathname == "/settings/account" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
<svg className={router.pathname == "/settings/account" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="truncate">
Account
</span>
</a>
</Link> */}
<ActiveLink href="/settings/password">
<a><KeyIcon /> Password</a>
</ActiveLink>
<ActiveLink href="/settings/embed">
<a><CodeIcon /> Embed</a>
</ActiveLink>
<ActiveLink href="/settings/teams">
<a><UserGroupIcon /> Teams</a>
</ActiveLink>
{/* Change/remove me, if you're self-hosting */}
<ActiveLink href="/settings/billing">
<a><CreditCardIcon /> Billing</a>
</ActiveLink>
{/* <Link href="/settings/notifications">
<a className={router.pathname == "/settings/notifications" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
<svg className={router.pathname == "/settings/notifications" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span className="truncate">
Notifications
</span>
</a>
</Link>
<Link href="/settings/billing">
<a className={router.pathname == "/settings/billing" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
<svg className={router.pathname == "/settings/billing" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
<span className="truncate">
Billing
</span>
</a>
</Link>
<Link href="/settings/integrations">
<a className={router.pathname == "/settings/integrations" ? "bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700 group border-l-4 px-3 py-2 flex items-center text-sm font-medium" : "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"}>
<svg className={router.pathname == "/settings/integrations" ? "text-blue-500 group-hover:text-blue-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" : "text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z" />
</svg>
<span className="truncate">
Integrations
</span>
</a>
</Link> */}
</nav>
</aside>
{props.children}
</div>
</div>
</div>
</main>
return (
<div className="max-w-6xl mx-auto">
<div>
<div className="sm:hidden">
<label htmlFor="tabs" className="sr-only">
Select a tab
</label>
<select
id="tabs"
name="tabs"
className="block w-full focus:ring-neutral-900 focus:border-neutral-900 border-gray-300 rounded-md"
defaultValue={tabs.find((tab) => tab.current).name}>
{tabs.map((tab) => (
<option key={tab.name}>{tab.name}</option>
))}
</select>
</div>
);
<div className="hidden sm:block">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<Link
key={tab.name}
href={tab.href}>
<a
className={classNames(
tab.current
? "border-neutral-900 text-neutral-900"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm"
)}
aria-current={tab.current ? "page" : undefined}>
<tab.icon
className={classNames(
tab.current ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 mr-2 h-5 w-5"
)}
aria-hidden="true"
/>
<span>{tab.name}</span>
</a>
</Link>
))}
</nav>
</div>
</div>
</div>
<main>{props.children}</main>
</div>
);
}

View File

@ -1,31 +1,68 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import { useRouter } from "next/router";
import { signOut, useSession } from "next-auth/client";
import { MenuIcon, XIcon } from "@heroicons/react/outline";
import { Dialog, Menu, Transition } from "@headlessui/react";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../lib/telemetry";
import { MenuIcon, SelectorIcon, XIcon } from "@heroicons/react/outline";
import {
CalendarIcon,
ClockIcon,
CogIcon,
PuzzleIcon,
SupportIcon,
ChatAltIcon,
LogoutIcon,
ExternalLinkIcon,
LinkIcon,
} from "@heroicons/react/solid";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
export default function Shell(props) {
const router = useRouter();
const [session, loading] = useSession();
const [profileDropdownExpanded, setProfileDropdownExpanded] = useState(false);
const [mobileMenuExpanded, setMobileMenuExpanded] = useState(false);
const telemetry = useTelemetry();
const navigation = [
{
name: "Event Types",
href: "/event-types",
icon: LinkIcon,
current: router.pathname.startsWith("/event-types"),
},
{
name: "Bookings",
href: "/bookings",
icon: ClockIcon,
current: router.pathname.startsWith("/bookings"),
},
{
name: "Availability",
href: "/availability",
icon: CalendarIcon,
current: router.pathname.startsWith("/availability"),
},
{
name: "Integrations",
href: "/integrations",
icon: PuzzleIcon,
current: router.pathname.startsWith("/integrations"),
},
{ name: "Settings", href: "/settings", icon: CogIcon, current: router.pathname.startsWith("/settings") },
];
const [sidebarOpen, setSidebarOpen] = useState(false);
useEffect(() => {
telemetry.withJitsu((jitsu) => {
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.pathname));
});
}, [telemetry]);
const toggleProfileDropdown = () => {
setProfileDropdownExpanded(!profileDropdownExpanded);
};
const toggleMobileMenu = () => {
setMobileMenuExpanded(!mobileMenuExpanded);
};
const logoutHandler = () => {
signOut({ redirect: false }).then(() => router.push("/auth/logout"));
};
@ -35,243 +72,283 @@ export default function Shell(props) {
}
return session ? (
<div>
<div className="dark:from-gray-900 dark:to-gray-900 bg-gradient-to-b from-blue-600 via-blue-600 to-blue-300 pb-32">
<nav className="dark:bg-gray-900 bg-blue-600">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="border-b border-blue-500 dark:border-gray-400">
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
<div className="flex items-center">
<div className="flex-shrink-0">
<Link href="/">
<a>
<img className="h-6" src="/calendso-white.svg" alt="Calendso" />
</a>
</Link>
</div>
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
<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>
</Link>
<Link href="/bookings">
<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 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>
</Link>
<Link href="/integrations">
<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>
</Link>
<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>
</Link>
</div>
</div>
</div>
<div className="hidden md:block">
<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 &amp; 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">
<div className="h-screen flex overflow-hidden bg-gray-100">
<Transition.Root show={sidebarOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 flex z-40 md:hidden"
open={sidebarOpen}
onClose={setSidebarOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Dialog.Overlay className="fixed inset-0 bg-gray-600 bg-opacity-75" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full">
<div className="relative flex-1 flex flex-col max-w-xs w-full bg-white">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="absolute top-0 right-0 -mr-12 pt-2">
<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" />}
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setSidebarOpen(false)}>
<span className="sr-only">Close sidebar</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</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>
</Transition.Child>
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<div className="flex-shrink-0 flex items-center px-4">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/workflow-logo-indigo-600-mark-gray-800-text.svg"
alt="Workflow"
/>
</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">
<nav className="mt-5 px-2 space-y-1">
{navigation.map((item) => (
<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
key={item.name}
href={item.href}
className={classNames(
item.current
? "bg-gray-100 text-gray-900"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
)}>
<item.icon
className={classNames(
item.current ? "text-gray-500" : "text-gray-400 group-hover:text-gray-500",
"mr-4 flex-shrink-0 h-6 w-6"
)}
aria-hidden="true"
/>
{item.name}
</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>
))}
</nav>
</div>
<div className="flex-shrink-0 flex border-t border-gray-200 p-4">
<a href="#" className="flex-shrink-0 group block">
<div className="flex items-center">
<div>
<img
className="inline-block h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</div>
<div className="ml-3">
<p className="text-base font-medium text-gray-700 group-hover:text-gray-900">
Tom Cook
</p>
<p className="text-sm font-medium text-gray-500 group-hover:text-gray-700">
View profile
</p>
</div>
</div>
</a>
</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>
</Transition.Child>
<div className="flex-shrink-0 w-14">{/* Force sidebar to shrink to fit close icon */}</div>
</Dialog>
</Transition.Root>
<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>
{/* Static sidebar for desktop */}
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex flex-col h-0 flex-1 border-r border-gray-200 bg-white">
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<img className="h-6 w-auto" src="/calendso-logo-word.svg" alt="Calendso" />
</div>
<nav className="mt-5 flex-1 px-2 bg-white space-y-1">
{navigation.map((item) => (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current
? "bg-neutral-100 text-neutral-900"
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
"group flex items-center px-2 py-2 text-sm font-medium rounded-sm"
)}>
<item.icon
className={classNames(
item.current ? "text-neutral-500" : "text-neutral-400 group-hover:text-neutral-500",
"mr-3 flex-shrink-0 h-5 w-5"
)}
aria-hidden="true"
/>
{item.name}
</a>
</Link>
))}
</nav>
</div>
<div className="flex-shrink-0 flex border-t border-gray-200 p-4">
{/* User account dropdown */}
<Menu as="div" className="w-full relative inline-block text-left">
{({ open }) => (
<>
<div>
<Menu.Button className="group w-full rounded-md text-sm text-left font-medium text-gray-700 focus:outline-none">
<span className="flex w-full justify-between items-center">
<span className="flex min-w-0 items-center justify-between space-x-3">
<img
className="w-10 h-10 bg-gray-300 rounded-full flex-shrink-0"
src={
session.user.image
? session.user.image
: "https://eu.ui-avatars.com/api/?background=fff&color=039be5&name=" +
encodeURIComponent(session.user.name || "")
}
alt=""
/>
<span className="flex-1 flex flex-col min-w-0">
<span className="text-gray-900 text-sm font-medium truncate">
{session.user.name}
</span>
<span className="text-neutral-500 font-normal text-sm truncate">{session.user.username}</span>
</span>
</span>
<SelectorIcon
className="flex-shrink-0 h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</span>
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95">
<Menu.Items
static
className="w-64 z-10 origin-top absolute bottom-14 left-0 mt-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-200 focus:outline-none">
<div className="py-1">
<a
href={"/" + session.user.username}
className="flex px-4 py-2 text-sm text-neutral-500 pb-6">
View public page <ExternalLinkIcon className="ml-1 w-4 h-4 text-neutral-400" />
<span className="absolute top-8 text-neutral-900 font-medium">
{window.location.hostname}/bailey
</span>
</a>
</div>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
href="#"
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-neutral-700",
"flex px-4 py-2 text-sm font-medium"
)}>
<SupportIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"mr-2 flex-shrink-0 h-5 w-5"
)}
aria-hidden="true"
/>
Help
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
href="#"
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-neutral-700",
"flex px-4 py-2 text-sm font-medium"
)}>
<ChatAltIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"mr-2 flex-shrink-0 h-5 w-5"
)}
aria-hidden="true"
/>
Feedback
</a>
)}
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
href="#"
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
"flex px-4 py-2 text-sm font-medium"
)}>
<LogoutIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"mr-2 flex-shrink-0 h-5 w-5"
)}
aria-hidden="true"
/>
Sign out
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
</div>
</div>
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<div className="md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3">
<button
className="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
onClick={() => setSidebarOpen(true)}>
<span className="sr-only">Open sidebar</span>
<MenuIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<main className="flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div className="py-6">
<div className="px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold text-gray-900">{props.heading}</h1>
</div>
<div className="px-4 sm:px-6 md:px-8">{props.children}</div>
</div>
</main>
</div>
</div>
) : null;
}

View File

@ -22,7 +22,7 @@ export default function TeamList(props) {
};
return (<div>
<ul className="border px-2 mb-2 rounded divide-y divide-gray-200">
<ul className="bg-white border px-2 mb-2 rounded divide-y divide-gray-200">
{props.teams.map(
(team: any) => <TeamListItem onChange={props.onChange} key={team.id} team={team} onActionSelect={
(action: string) => selectAction(action, team)
@ -39,4 +39,4 @@ export default function TeamList(props) {
onExit={() => setShowMemberInvitationModal(false)}></MemberInvitationModal>
}
</div>);
}
}

View File

@ -26,25 +26,25 @@ export default function TeamListItem(props) {
<div>
<UsersIcon className="text-gray-400 group-hover:text-gray-500 flex-shrink-0 -mt-4 mr-2 h-6 w-6 inline"/>
<div className="inline-block -mt-1">
<span className="font-bold text-blue-700 text-sm">{props.team.name}</span>
<span className="font-bold text-neutral-700 text-sm">{props.team.name}</span>
<span className="text-xs text-gray-400 -mt-1 block capitalize">{props.team.role.toLowerCase()}</span>
</div>
</div>
{props.team.role === 'INVITEE' && <div>
<button className="btn-sm bg-transparent text-green-500 border border-green-500 px-3 py-1 rounded ml-2" onClick={acceptInvite}>Accept invitation</button>
<button className="btn-sm bg-transparent text-green-500 border border-green-500 px-3 py-1 rounded-sm ml-2" onClick={acceptInvite}>Accept invitation</button>
<button className="btn-sm bg-transparent px-2 py-1 ml-1">
<TrashIcon className="h-6 w-6 inline text-gray-400 -mt-1" onClick={declineInvite} />
</button>
</div>}
{props.team.role === 'MEMBER' && <div>
<button onClick={declineInvite} className="btn-sm bg-transparent text-gray-400 border border-gray-400 px-3 py-1 rounded ml-2">Leave</button>
<button onClick={declineInvite} className="btn-sm bg-transparent text-gray-400 border border-gray-400 px-3 py-1 rounded-sm ml-2">Leave</button>
</div>}
{props.team.role === 'OWNER' && <div>
<Dropdown className="relative inline-block text-left">
<button className="btn-sm bg-transparent text-gray-400 px-3 py-1 rounded ml-2">
<button className="btn-sm bg-transparent text-gray-400 px-3 py-1 rounded-sm ml-2">
<CogIcon className="h-6 w-6 inline text-gray-400" />
</button>
<ul role="menu" className="z-10 origin-top-right absolute right-0 w-36 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<ul role="menu" className="z-10 origin-top-right absolute right-0 w-36 rounded-sm shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<li className="text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">
<a className="block px-4 py-2" onClick={() => props.onActionSelect('invite')}>Invite member(s)</a>
</li>
@ -64,14 +64,14 @@ export default function TeamListItem(props) {
<td className="py-1 pl-2">Alex van Andel ({ member.email })</td>
<td>Owner</td>
<td className="text-right p-1">
<button className="btn-sm text-xs bg-transparent text-red-400 border border-red-400 px-3 py-1 rounded ml-2"><UserRemoveIcon className="text-red-400 group-hover:text-gray-500 flex-shrink-0 -mt-1 mr-1 h-4 w-4 inline"/>Remove</button>
<button className="btn-sm text-xs bg-transparent text-red-400 border border-red-400 px-3 py-1 rounded-sm ml-2"><UserRemoveIcon className="text-red-400 group-hover:text-gray-500 flex-shrink-0 -mt-1 mr-1 h-4 w-4 inline"/>Remove</button>
</td>
</tr>)}
</tbody>
</table>
</div>}
<button className="btn-sm bg-transparent text-gray-400 border border-gray-400 px-3 py-1 rounded"><UserAddIcon className="text-gray-400 group-hover:text-gray-500 flex-shrink-0 -mt-1 h-6 w-6 inline"/> Invite member</button>
<button className="btn-sm bg-transparent text-red-400 border border-red-400 px-3 py-1 rounded ml-2">Disband</button>
<button className="btn-sm bg-transparent text-gray-400 border border-gray-400 px-3 py-1 rounded-sm"><UserAddIcon className="text-gray-400 group-hover:text-gray-500 flex-shrink-0 -mt-1 h-6 w-6 inline"/> Invite member</button>
<button className="btn-sm bg-transparent text-red-400 border border-red-400 px-3 py-1 rounded-sm ml-2">Disband</button>
</div>}*/}
</li>);
}
}

View File

@ -95,9 +95,9 @@ export const Scheduler = ({
return (
<div>
<div className="rounded border flex">
<div className="flex">
<div className="w-full">
<div className=" p-2">
<div className="">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
Timezone
</label>

View File

@ -7,7 +7,7 @@ const UsernameInput = React.forwardRef((props, ref) => (
Username
</label>
<div className="mt-1 rounded-md shadow-sm flex">
<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-sm px-3 inline-flex items-center text-gray-500 sm:text-sm">
{typeof window !== "undefined" && window.location.hostname}/
</span>
<input
@ -18,7 +18,7 @@ const UsernameInput = React.forwardRef((props, ref) => (
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"
className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-sm sm:text-sm border-gray-300 lowercase"
/>
</div>
</div>

View File

@ -33,7 +33,7 @@ export default function Error() {
</div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login">
<a className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm">
<a className="inline-flex justify-center w-full rounded-sm border border-transparent shadow-sm px-4 py-2 bg-neutral-900 text-base font-medium text-white hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500 sm:text-sm">
Go back to the login page
</a>
</Link>
@ -42,4 +42,4 @@ export default function Error() {
</div>
</div>
);
}
}

View File

@ -4,21 +4,22 @@ import { getCsrfToken } from "next-auth/client";
export default function Login({ csrfToken }) {
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="min-h-screen bg-neutral-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<Head>
<title>Login</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
<img className="h-6 mx-auto" src="/calendso-logo-word.svg" alt="Calendso Logo" />
<h2 className="mt-6 text-center text-3xl font-bold text-neutral-900">Sign in to your account</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 mx-2 shadow rounded-lg sm:px-10">
<div className="bg-white py-8 px-4 mx-2 rounded-sm sm:px-10 border border-neutral-200">
<form className="space-y-6" method="post" action="/api/auth/callback/credentials">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
<label htmlFor="email" className="block text-sm font-medium text-neutral-700">
Email address
</label>
<div className="mt-1">
@ -27,26 +28,33 @@ export default function Login({ csrfToken }) {
name="email"
type="email"
autoComplete="email"
placeholder="john.doe@example.com"
required
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
className="appearance-none block w-full px-3 py-2 border border-neutral-300 rounded-sm shadow-sm placeholder-gray-400 focus:outline-none focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<div className="flex">
<div className="w-1/2">
<label htmlFor="password" className="block text-sm font-medium text-neutral-700">
Password
</label>
</div>
<div className="w-1/2 text-right">
<Link href="/auth/forgot-password">
<a className="font-medium text-secondary-600 text-sm">Forgot?</a>
</Link>
</div>
</div>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="•••••••••••••"
required
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
className="appearance-none block w-full px-3 py-2 border border-neutral-300 rounded-sm shadow-sm placeholder-gray-400 focus:outline-none focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
/>
</div>
</div>
@ -54,19 +62,15 @@ export default function Login({ csrfToken }) {
<div className="space-y-2">
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Sign in
</button>
<Link href="/auth/forgot-password">
<button
type="button"
className="w-full flex justify-center py-2 px-4 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Forgot Password?
</button>
</Link>
</div>
</form>
</div>
<div className="mt-4 text-neutral-600 text-center text-sm">
Don&apos;t have an account? <a href="#" className="font-medium text-neutral-900">Create an account</a>
</div>
</div>
</div>
);

View File

@ -1,424 +1,348 @@
import Head from 'next/head';
import Link from 'next/link';
import prisma from '../../lib/prisma';
import Modal from '../../components/Modal';
import Shell from '../../components/Shell';
import {useRouter} from 'next/router';
import {useRef, useState} from 'react';
import {getSession, useSession} from 'next-auth/client';
import {ClockIcon, PlusIcon} from '@heroicons/react/outline';
import Head from "next/head";
import Link from "next/link";
import prisma from "../../lib/prisma";
import Modal from "../../components/Modal";
import Shell from "../../components/Shell";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { getSession, useSession } from "next-auth/client";
import { ClockIcon, PlusIcon } from "@heroicons/react/outline";
export default function Availability(props) {
const [ session, loading ] = useSession();
const router = useRouter();
const [showAddModal, setShowAddModal] = useState(false);
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [showChangeTimesModal, setShowChangeTimesModal] = useState(false);
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
const isHiddenRef = useRef<HTMLInputElement>();
const [session, loading] = useSession();
const router = useRouter();
const [showAddModal, setShowAddModal] = useState(false);
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [showChangeTimesModal, setShowChangeTimesModal] = useState(false);
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
const isHiddenRef = useRef<HTMLInputElement>();
const startHoursRef = useRef<HTMLInputElement>();
const startMinsRef = useRef<HTMLInputElement>();
const endHoursRef = useRef<HTMLInputElement>();
const endMinsRef = useRef<HTMLInputElement>();
const bufferHoursRef = useRef<HTMLInputElement>();
const bufferMinsRef = useRef<HTMLInputElement>();
const startHoursRef = useRef<HTMLInputElement>();
const startMinsRef = useRef<HTMLInputElement>();
const endHoursRef = useRef<HTMLInputElement>();
const endMinsRef = useRef<HTMLInputElement>();
const bufferHoursRef = useRef<HTMLInputElement>();
const bufferMinsRef = useRef<HTMLInputElement>();
if (loading) {
return <div className="loader"></div>;
if (loading) {
return <div className="loader"></div>;
}
function toggleAddModal() {
setShowAddModal(!showAddModal);
}
function toggleChangeTimesModal() {
setShowChangeTimesModal(!showChangeTimesModal);
}
const closeSuccessModal = () => {
setSuccessModalOpen(false);
router.replace(router.asPath);
};
function convertMinsToHrsMins(mins) {
let h = Math.floor(mins / 60);
let m = mins % 60;
h = h < 10 ? "0" + h : h;
m = m < 10 ? "0" + m : m;
return `${h}:${m}`;
}
async function createEventTypeHandler(event) {
event.preventDefault();
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = lengthRef.current.value;
const enteredIsHidden = isHiddenRef.current.checked;
// TODO: Add validation
const response = await fetch("/api/availability/eventtype", {
method: "POST",
body: JSON.stringify({
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
hidden: enteredIsHidden,
}),
headers: {
"Content-Type": "application/json",
},
});
if (enteredTitle && enteredLength) {
router.replace(router.asPath);
toggleAddModal();
}
}
function toggleAddModal() {
setShowAddModal(!showAddModal);
}
async function updateStartEndTimesHandler(event) {
event.preventDefault();
function toggleChangeTimesModal() {
setShowChangeTimesModal(!showChangeTimesModal);
}
const enteredStartHours = parseInt(startHoursRef.current.value);
const enteredStartMins = parseInt(startMinsRef.current.value);
const enteredEndHours = parseInt(endHoursRef.current.value);
const enteredEndMins = parseInt(endMinsRef.current.value);
const enteredBufferHours = parseInt(bufferHoursRef.current.value);
const enteredBufferMins = parseInt(bufferMinsRef.current.value);
const closeSuccessModal = () => { setSuccessModalOpen(false); router.replace(router.asPath); }
const startMins = enteredStartHours * 60 + enteredStartMins;
const endMins = enteredEndHours * 60 + enteredEndMins;
const bufferMins = enteredBufferHours * 60 + enteredBufferMins;
function convertMinsToHrsMins (mins) {
let h = Math.floor(mins / 60);
let m = mins % 60;
h = h < 10 ? '0' + h : h;
m = m < 10 ? '0' + m : m;
return `${h}:${m}`;
}
// TODO: Add validation
async function createEventTypeHandler(event) {
event.preventDefault();
const response = await fetch("/api/availability/day", {
method: "PATCH",
body: JSON.stringify({ start: startMins, end: endMins, buffer: bufferMins }),
headers: {
"Content-Type": "application/json",
},
});
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = lengthRef.current.value;
const enteredIsHidden = isHiddenRef.current.checked;
setShowChangeTimesModal(false);
setSuccessModalOpen(true);
}
// TODO: Add validation
const response = await fetch('/api/availability/eventtype', {
method: 'POST',
body: JSON.stringify({title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden}),
headers: {
'Content-Type': 'application/json'
}
});
if (enteredTitle && enteredLength) {
router.replace(router.asPath);
toggleAddModal();
}
}
async function updateStartEndTimesHandler(event) {
event.preventDefault();
const enteredStartHours = parseInt(startHoursRef.current.value);
const enteredStartMins = parseInt(startMinsRef.current.value);
const enteredEndHours = parseInt(endHoursRef.current.value);
const enteredEndMins = parseInt(endMinsRef.current.value);
const enteredBufferHours = parseInt(bufferHoursRef.current.value);
const enteredBufferMins = parseInt(bufferMinsRef.current.value);
const startMins = enteredStartHours * 60 + enteredStartMins;
const endMins = enteredEndHours * 60 + enteredEndMins;
const bufferMins = enteredBufferHours * 60 + enteredBufferMins;
// TODO: Add validation
const response = await fetch('/api/availability/day', {
method: 'PATCH',
body: JSON.stringify({start: startMins, end: endMins, buffer: bufferMins}),
headers: {
'Content-Type': 'application/json'
}
});
setShowChangeTimesModal(false);
setSuccessModalOpen(true);
}
return(
<div>
<Head>
<title>Availability | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Availability">
<div className="mb-4 sm:flex sm:items-center sm:justify-between">
<h3 className="text-lg leading-6 font-medium text-white">
Event Types
</h3>
<div className="mt-3 sm:mt-0 sm:ml-4">
<button onClick={toggleAddModal} type="button" className="btn-sm btn-white">
New event type
</button>
</div>
</div>
<div className="flex flex-col mb-8">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block max-w-full min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Length
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{props.types.map((eventType) =>
<tr key={eventType.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 align-top">
{eventType.title}
{eventType.hidden &&
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
Hidden
</span>
}
</td>
<td className="px-6 py-4 text-sm text-gray-500 align-top">
{eventType.description}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 align-top">
{eventType.length} minutes
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium align-top">
<Link href={"/" + props.user.username + "/" + eventType.slug}><a target="_blank" className="text-blue-600 hover:text-blue-900 mr-2">View</a></Link>
<Link href={"/availability/event/" + eventType.id}><a className="text-blue-600 hover:text-blue-900">Edit</a></Link>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div className="flex">
<div className="w-1/2 mr-2 bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Change the start and end times of your day
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end at {convertMinsToHrsMins(props.user.endTime)}.
</p>
</div>
<div className="mt-5">
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-primary">
Change available times
</button>
</div>
</div>
</div>
<div className="w-1/2 ml-2 bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Something doesn&apos;t look right?
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Troubleshoot your availability to explore why your times are showing as they are.
</p>
</div>
<div className="mt-5">
<Link href="/availability/troubleshoot">
<a className="btn btn-primary">
Launch troubleshooter
</a>
</Link>
</div>
</div>
</div>
</div>
{showAddModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="h-6 w-6 text-blue-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Add a new event type
</h3>
<div>
<p className="text-sm text-gray-500">
Create a new event type for people to book times with.
</p>
</div>
</div>
</div>
<form onSubmit={createEventTypeHandler}>
<div>
<div className="mb-4">
<label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
<div className="mt-1">
<input ref={titleRef} type="text" name="title" id="title" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Quick Chat" />
</div>
</div>
<div className="mb-4">
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">URL</label>
<div className="mt-1">
<div className="flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
{location.hostname}/{props.user.username}/
</span>
<input
ref={slugRef}
type="text"
name="slug"
id="slug"
required
className="flex-1 block w-full focus:ring-blue-500 focus:border-blue-500 min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
/>
</div>
</div>
</div>
<div className="mb-4">
<label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label>
<div className="mt-1">
<textarea ref={descriptionRef} name="description" id="description" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="A quick video meeting."></textarea>
</div>
</div>
<div className="mb-4">
<label htmlFor="length" className="block text-sm font-medium text-gray-700">Length</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input ref={lengthRef} type="number" name="length" id="length" required className="focus:ring-blue-500 focus:border-blue-500 block w-full pr-20 sm:text-sm border-gray-300 rounded-md" placeholder="15" />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
minutes
</div>
</div>
</div>
</div>
<div className="my-8">
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
ref={isHiddenRef}
id="ishidden"
name="ishidden"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="ishidden" className="font-medium text-gray-700">
Hide this event type
</label>
<p className="text-gray-500">Hide the event type from your page, so it can only be booked through it&apos;s URL.</p>
</div>
</div>
</div>
{/* TODO: Add an error message when required input fields empty*/}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
Create
</button>
<button onClick={toggleAddModal} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
}
{showChangeTimesModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<ClockIcon className="h-6 w-6 text-blue-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Change your available times
</h3>
<div>
<p className="text-sm text-gray-500">
Set the start and end time of your day and a minimum buffer between your meetings.
</p>
</div>
</div>
</div>
<form onSubmit={updateStartEndTimesHandler}>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
<div>
<label htmlFor="hours" className="sr-only">Hours</label>
<input ref={startHoursRef} type="number" name="hours" id="hours" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="9" defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[0]} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">Minutes</label>
<input ref={startMinsRef} type="number" name="minutes" id="minutes" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="30" defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[1]} />
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
<div>
<label htmlFor="hours" className="sr-only">Hours</label>
<input ref={endHoursRef} type="number" name="hours" id="hours" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="17" defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[0]} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">Minutes</label>
<input ref={endMinsRef} type="number" name="minutes" id="minutes" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="30" defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[1]} />
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Buffer</label>
<div>
<label htmlFor="hours" className="sr-only">Hours</label>
<input ref={bufferHoursRef} type="number" name="hours" id="hours" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="0" defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[0]} />
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">Minutes</label>
<input ref={bufferMinsRef} type="number" name="minutes" id="minutes" className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="10" defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[1]} />
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
Update
</button>
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
}
<Modal heading="Start and end times changed" description="The start and end times for your day have been changed successfully." open={successModalOpen} handleClose={closeSuccessModal} />
</Shell>
return (
<div>
<Head>
<title>Availability | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Availability">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">Configure times when you are available for bookings.</p>
</div>
);
<div className="flex">
<div className="w-1/2 mr-2 bg-white shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Change the start and end times of your day
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Currently, your day is set to start at {convertMinsToHrsMins(props.user.startTime)} and end
at {convertMinsToHrsMins(props.user.endTime)}.
</p>
</div>
<div className="mt-5">
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-primary">
Change available times
</button>
</div>
</div>
</div>
<div className="w-1/2 ml-2 bg-white shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Something doesn&apos;t look right?
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>Troubleshoot your availability to explore why your times are showing as they are.</p>
</div>
<div className="mt-5">
<Link href="/availability/troubleshoot">
<a className="btn btn-primary">Launch troubleshooter</a>
</Link>
</div>
</div>
</div>
</div>
{showChangeTimesModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<ClockIcon className="h-6 w-6 text-neutral-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Change your available times
</h3>
<div>
<p className="text-sm text-gray-500">
Set the start and end time of your day and a minimum buffer between your meetings.
</p>
</div>
</div>
</div>
<form onSubmit={updateStartEndTimesHandler}>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Start time</label>
<div>
<label htmlFor="hours" className="sr-only">
Hours
</label>
<input
ref={startHoursRef}
type="number"
name="hours"
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="9"
defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">
Minutes
</label>
<input
ref={startMinsRef}
type="number"
name="minutes"
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
defaultValue={convertMinsToHrsMins(props.user.startTime).split(":")[1]}
/>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">End time</label>
<div>
<label htmlFor="hours" className="sr-only">
Hours
</label>
<input
ref={endHoursRef}
type="number"
name="hours"
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="17"
defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">
Minutes
</label>
<input
ref={endMinsRef}
type="number"
name="minutes"
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="30"
defaultValue={convertMinsToHrsMins(props.user.endTime).split(":")[1]}
/>
</div>
</div>
<div className="flex mb-4">
<label className="w-1/4 pt-2 block text-sm font-medium text-gray-700">Buffer</label>
<div>
<label htmlFor="hours" className="sr-only">
Hours
</label>
<input
ref={bufferHoursRef}
type="number"
name="hours"
id="hours"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="0"
defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[0]}
/>
</div>
<span className="mx-2 pt-1">:</span>
<div>
<label htmlFor="minutes" className="sr-only">
Minutes
</label>
<input
ref={bufferMinsRef}
type="number"
name="minutes"
id="minutes"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="10"
defaultValue={convertMinsToHrsMins(props.user.bufferTime).split(":")[1]}
/>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
Update
</button>
<button onClick={toggleChangeTimesModal} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
)}
<Modal
heading="Start and end times changed"
description="The start and end times for your day have been changed successfully."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
</Shell>
</div>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
}
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
startTime: true,
endTime: true,
bufferTime: true
}
});
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
startTime: true,
endTime: true,
bufferTime: true,
},
});
const types = await prisma.eventType.findMany({
where: {
userId: user.id,
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true
}
});
return {
props: {user, types}, // will be passed to the page component as props
}
const types = await prisma.eventType.findMany({
where: {
userId: user.id,
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true,
},
});
return {
props: { user, types }, // will be passed to the page component as props
};
}

View File

@ -51,25 +51,30 @@ export default function Troubleshoot({ user }) {
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Troubleshoot">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">
Understand why certain times are available and others are blocked.
</p>
</div>
<div className="bg-white overflow-hidden shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
Here is an overview of your day on {selectedDate.format("D MMMM YYYY")}:
<small className="block text-gray-400">Tip: Hover over the bold times for a full timestamp</small>
<small className="block text-neutral-400">Tip: Hover over the bold times for a full timestamp</small>
<div className="mt-4 space-y-4">
<div className="bg-gray-600 overflow-hidden rounded-lg">
<div className="bg-neutral-600 overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
Your day starts at {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{availability.map((slot) => (
<div key={slot.start} className="bg-gray-100 overflow-hidden rounded-lg">
<div className="px-4 py-5 sm:p-6 text-gray-600">
Your calendar shows you as busy between <span className="font-medium text-gray-800" title={slot.start}>{dayjs(slot.start).format("HH:mm")}</span> and <span className="font-medium text-gray-800" title={slot.end}>{dayjs(slot.end).format("HH:mm")}</span> on {dayjs(slot.start).format("D MMMM YYYY")}
<div key={slot.start} className="bg-neutral-100 overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:p-6 text-neutral-600">
Your calendar shows you as busy between <span className="font-medium text-neutral-800" title={slot.start}>{dayjs(slot.start).format("HH:mm")}</span> and <span className="font-medium text-neutral-800" title={slot.end}>{dayjs(slot.end).format("HH:mm")}</span> on {dayjs(slot.start).format("D MMMM YYYY")}
</div>
</div>
))}
{availability.length === 0 && <div className="loader"></div>}
<div className="bg-gray-600 overflow-hidden rounded-lg">
<div className="bg-neutral-600 overflow-hidden rounded-sm">
<div className="px-4 sm:px-6 py-2 text-white">
Your day ends at {convertMinsToHrsMins(user.endTime)}
</div>

View File

@ -32,33 +32,16 @@ export default function Bookings({ bookings }) {
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Bookings">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">
See upcoming and past events booked through your event type links.
</p>
</div>
<div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-sm">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Person
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Event
</th>
{/* <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th> */}
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{bookings
.filter((booking) => !booking.confirmed && !booking.rejected)
@ -70,7 +53,7 @@ export default function Bookings({ bookings }) {
"px-6 py-4 whitespace-nowrap" + (booking.rejected ? " line-through" : "")
}>
{!booking.confirmed && !booking.rejected && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-600 text-gray-100">
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
Unconfirmed
</span>
)}
@ -83,8 +66,8 @@ export default function Bookings({ bookings }) {
className={
"px-6 py-4 max-w-20 w-full" + (booking.rejected ? " line-through" : "")
}>
<div className="text-sm text-gray-900">{booking.title}</div>
<div className="text-sm text-gray-500">{booking.description}</div>
<div className="text-sm text-neutral-900 font-medium">{booking.title}</div>
<div className="text-sm text-neutral-500">You and {booking.attendees[0].name}</div>
</td>
{/* <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">
@ -109,14 +92,14 @@ export default function Bookings({ bookings }) {
{booking.confirmed && !booking.rejected && (
<>
<a
href={window.location.href + "/../reschedule/" + booking.uid}
className="text-blue-600 hover:text-blue-900">
Reschedule
href={window.location.href + "/../cancel/" + booking.uid}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-neutral-700 bg-white hover:bg-neutral-100 border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mr-2">
Cancel
</a>
<a
href={window.location.href + "/../cancel/" + booking.uid}
className="ml-4 text-blue-600 hover:text-blue-900">
Cancel
href={window.location.href + "/../reschedule/" + booking.uid}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-neutral-700 bg-white hover:bg-neutral-100 border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 mr-2">
Reschedule
</a>
</>
)}

1201
pages/event-types/[type].tsx Normal file

File diff suppressed because it is too large Load Diff

662
pages/event-types/index.tsx Normal file
View File

@ -0,0 +1,662 @@
import Head from "next/head";
import Link from "next/link";
import prisma from "../../lib/prisma";
import Shell from "../../components/Shell";
import { useRouter } from "next/router";
import { getSession, useSession } from "next-auth/client";
import { Fragment, useRef, useState } from "react";
import { Menu, Transition } from "@headlessui/react";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
import {
ClockIcon,
DotsHorizontalIcon,
ExternalLinkIcon,
InformationCircleIcon,
LinkIcon,
PlusIcon,
UserIcon,
} from "@heroicons/react/solid";
export default function Availability({ user, types }) {
const [session, loading] = useSession();
const router = useRouter();
const [showAddModal, setShowAddModal] = useState(false);
const titleRef = useRef<HTMLInputElement>();
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
async function createEventTypeHandler(event) {
event.preventDefault();
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = lengthRef.current.value;
// TODO: Add validation
const response = await fetch("/api/availability/eventtype", {
method: "POST",
body: JSON.stringify({
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
}),
headers: {
"Content-Type": "application/json",
},
});
if (enteredTitle && enteredLength) {
router.replace(router.asPath);
toggleAddModal();
}
}
function toggleAddModal() {
setShowAddModal(!showAddModal);
}
if (loading) {
return <div className="loader"></div>;
}
return (
<div>
<Head>
<title>Event Types | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Event Types">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">
Create events to share for people to book on your calendar.
</p>
</div>
<button
onClick={toggleAddModal}
className="absolute top-8 right-8 flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
<PlusIcon className="w-5 h-5 mr-1" />
New event type
</button>
<div className="bg-white shadow overflow-hidden sm:rounded-sm">
<ul className="divide-y divide-neutral-200">
{types.map((type) => (
<li key={type.id}>
<Link href={"/event-types/" + type.id}>
<a className="block hover:bg-neutral-50">
<div className="px-4 py-4 flex items-center sm:px-6">
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
<div className="truncate">
<div className="flex text-sm">
<p className="font-medium text-neutral-900 truncate">{type.title}</p>
{type.hidden && (
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
Hidden
</span>
)}
</div>
<div className="mt-2 flex space-x-4">
<div className="flex items-center text-sm text-neutral-500">
<ClockIcon
className="flex-shrink-0 mr-1.5 h-5 w-5 text-neutral-400"
aria-hidden="true"
/>
<p>{type.length}m</p>
</div>
<div className="flex items-center text-sm text-neutral-500">
<UserIcon
className="flex-shrink-0 mr-1.5 h-5 w-5 text-neutral-400"
aria-hidden="true"
/>
<p>1-on-1</p>
</div>
<div className="flex items-center text-sm text-neutral-500">
<InformationCircleIcon
className="flex-shrink-0 mr-1.5 h-5 w-5 text-neutral-400"
aria-hidden="true"
/>
<p>{type.description.substring(0, 100)}</p>
</div>
</div>
</div>
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
<div className="flex overflow-hidden space-x-5">
<Link href={"/" + session.user.username + "/" + type.slug}>
<a className="text-neutral-400">
<ExternalLinkIcon className="w-5 h-5" />
</a>
</Link>
<button
onClick={() => {
navigator.clipboard.writeText(
window.location.hostname + "/" + session.user.username + "/" + type.slug
);
}}
className="text-neutral-400">
<LinkIcon className="w-5 h-5" />
</button>
</div>
</div>
</div>
<div className="ml-5 flex-shrink-0">
<Menu as="div" className="inline-block text-left">
{({ open }) => (
<>
<div>
<Menu.Button className="text-neutral-400 mt-1">
<span className="sr-only">Open options</span>
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95">
<Menu.Items
static
className="origin-top-right absolute right-0 mt-2 w-56 rounded-sm shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none divide-y divide-neutral-100">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
href={"/" + session.user.username + "/" + type.slug}
target="_blank"
rel="noreferrer"
className={classNames(
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm font-medium"
)}>
<ExternalLinkIcon
className="mr-3 h-5 w-5 text-neutral-400 group-hover:text-neutral-500"
aria-hidden="true"
/>
Preview
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
onClick={() => {
navigator.clipboard.writeText(
window.location.hostname +
"/" +
session.user.username +
"/" +
type.slug
);
}}
className={classNames(
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm w-full font-medium"
)}>
<LinkIcon
className="mr-3 h-5 w-5 text-neutral-400 group-hover:text-neutral-500"
aria-hidden="true"
/>
Copy link to event
</button>
)}
</Menu.Item>
{/*<Menu.Item>*/}
{/* {({ active }) => (*/}
{/* <a*/}
{/* href="#"*/}
{/* className={classNames(*/}
{/* active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",*/}
{/* "group flex items-center px-4 py-2 text-sm font-medium"*/}
{/* )}>*/}
{/* <DuplicateIcon*/}
{/* className="mr-3 h-5 w-5 text-neutral-400 group-hover:text-neutral-500"*/}
{/* aria-hidden="true"*/}
{/* />*/}
{/* Duplicate*/}
{/* </a>*/}
{/* )}*/}
{/*</Menu.Item>*/}
</div>
{/*<div className="py-1">*/}
{/* <Menu.Item>*/}
{/* {({ active }) => (*/}
{/* <a*/}
{/* href="#"*/}
{/* className={classNames(*/}
{/* active ? "bg-red-100 text-red-900" : "text-red-700",*/}
{/* "group flex items-center px-4 py-2 text-sm font-medium"*/}
{/* )}>*/}
{/* <TrashIcon*/}
{/* className="mr-3 h-5 w-5 text-red-400 group-hover:text-red-700"*/}
{/* aria-hidden="true"*/}
{/* />*/}
{/* Delete*/}
{/* </a>*/}
{/* )}*/}
{/* </Menu.Item>*/}
{/*</div>*/}
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
</a>
</Link>
</li>
))}
</ul>
</div>
{types.length === 0 && (
<div className="text-center max-w-lg mx-auto">
<svg
className="mx-auto mb-4 w-32 h-32"
viewBox="0 0 132 132"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect
x="1.48387"
y="1.48387"
width="129.032"
height="129.032"
rx="64.5161"
fill="white"
stroke="white"
strokeWidth="1.03226"
/>
<mask
id="mask0"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="2"
y="2"
width="128"
height="128">
<rect x="2" y="2" width="128" height="128" rx="64" fill="white" />
</mask>
<g mask="url(#mask0)">
<rect x="56.1936" y="40.1936" width="20.129" height="2.06452" rx="0.516129" fill="#708097" />
<rect x="47.9355" y="44.8387" width="36.6452" height="2.06452" rx="0.516129" fill="#C6CCD5" />
<g filter="url(#filter0_dd)">
<rect width="115.84" height="83.2303" transform="translate(8.07983 53.52)" fill="#F7F8F9" />
<path
d="M15.7699 61.589V63.5013H16.1023V62.1847H16.1201L16.6486 63.4957H16.8969L17.4254 62.1875H17.4432V63.5013H17.7756V61.589H17.3517L16.7839 62.9747H16.7615L16.1938 61.589H15.7699ZM19.993 62.5451C19.993 61.927 19.6158 61.5628 19.1144 61.5628C18.612 61.5628 18.2357 61.927 18.2357 62.5451C18.2357 63.1623 18.612 63.5274 19.1144 63.5274C19.6158 63.5274 19.993 63.1633 19.993 62.5451ZM19.6447 62.5451C19.6447 62.9803 19.4262 63.2165 19.1144 63.2165C18.8034 63.2165 18.584 62.9803 18.584 62.5451C18.584 62.11 18.8034 61.8738 19.1144 61.8738C19.4262 61.8738 19.6447 62.11 19.6447 62.5451Z"
fill="#657388"
/>
<path
d="M32.1112 61.8794H32.7022V63.5013H33.0459V61.8794H33.6369V61.589H32.1112V61.8794ZM35.268 61.589V62.8094C35.268 63.0494 35.1008 63.2212 34.8385 63.2212C34.5751 63.2212 34.4089 63.0494 34.4089 62.8094V61.589H34.0625V62.8383C34.0625 63.2492 34.3706 63.5302 34.8385 63.5302C35.3044 63.5302 35.6144 63.2492 35.6144 62.8383V61.589H35.268Z"
fill="#657388"
/>
<path
d="M48.3554 63.5013H48.6971L49.0809 62.1595H49.0958L49.4786 63.5013H49.8204L50.3601 61.589H49.9875L49.643 62.9952H49.6262L49.2573 61.589H48.9184L48.5505 62.9943H48.5328L48.1882 61.589H47.8157L48.3554 63.5013ZM50.7318 63.5013H51.983V63.2109H51.0782V62.6889H51.9111V62.3985H51.0782V61.8794H51.9755V61.589H50.7318V63.5013Z"
fill="#657388"
/>
<path
d="M64.1925 61.8794H64.7836V63.5013H65.1272V61.8794H65.7183V61.589H64.1925V61.8794ZM66.1439 63.5013H66.4903V62.6889H67.3764V63.5013H67.7237V61.589H67.3764V62.3985H66.4903V61.589H66.1439V63.5013Z"
fill="#657388"
/>
<path
d="M80.545 63.5013H80.8914V62.6889H81.686V62.3985H80.8914V61.8794H81.7701V61.589H80.545V63.5013ZM82.2171 63.5013H82.5636V62.801H82.9165L83.2919 63.5013H83.6784L83.2648 62.7431C83.4898 62.6525 83.6084 62.4602 83.6084 62.2006C83.6084 61.8355 83.3731 61.589 82.9342 61.589H82.2171V63.5013ZM82.5636 62.5134V61.8784H82.881C83.1397 61.8784 83.2555 61.997 83.2555 62.2006C83.2555 62.4041 83.1397 62.5134 82.8829 62.5134H82.5636Z"
fill="#657388"
/>
<path
d="M97.4678 62.1147H97.8011C97.7946 61.7916 97.5191 61.5628 97.112 61.5628C96.7105 61.5628 96.4089 61.7888 96.4098 62.1268C96.4098 62.4013 96.605 62.5591 96.9197 62.6404L97.1372 62.6964C97.3436 62.7487 97.4799 62.8131 97.4808 62.9616C97.4799 63.125 97.3249 63.2342 97.0989 63.2342C96.8823 63.2342 96.7142 63.1371 96.7002 62.9364H96.3594C96.3734 63.3164 96.6563 63.5302 97.1017 63.5302C97.5602 63.5302 97.8263 63.3015 97.8273 62.9644C97.8263 62.6329 97.5527 62.4816 97.2651 62.4135L97.0859 62.3687C96.929 62.3313 96.7591 62.265 96.7609 62.1053C96.7619 61.9615 96.8907 61.856 97.1073 61.856C97.3137 61.856 97.45 61.9522 97.4678 62.1147ZM98.4823 63.5013L98.6401 63.0297H99.3591L99.5178 63.5013H99.8876L99.2134 61.589H98.7858L98.1126 63.5013H98.4823ZM98.7335 62.7515L98.9921 61.9812H99.0071L99.2657 62.7515H98.7335Z"
fill="#657388"
/>
<path
d="M113.487 62.1147H113.82C113.814 61.7916 113.538 61.5628 113.131 61.5628C112.73 61.5628 112.428 61.7888 112.429 62.1268C112.429 62.4013 112.624 62.5591 112.939 62.6404L113.157 62.6964C113.363 62.7487 113.499 62.8131 113.5 62.9616C113.499 63.125 113.344 63.2342 113.118 63.2342C112.902 63.2342 112.734 63.1371 112.72 62.9364H112.379C112.393 63.3164 112.676 63.5302 113.121 63.5302C113.58 63.5302 113.846 63.3015 113.847 62.9644C113.846 62.6329 113.572 62.4816 113.285 62.4135L113.105 62.3687C112.948 62.3313 112.778 62.265 112.78 62.1053C112.781 61.9615 112.91 61.856 113.127 61.856C113.333 61.856 113.469 61.9522 113.487 62.1147ZM115.492 61.589V62.8094C115.492 63.0494 115.325 63.2212 115.063 63.2212C114.8 63.2212 114.633 63.0494 114.633 62.8094V61.589H114.287V62.8383C114.287 63.2492 114.595 63.5302 115.063 63.5302C115.529 63.5302 115.839 63.2492 115.839 62.8383V61.589H115.492Z"
fill="#657388"
/>
<rect x="9.83276" y="70.2902" width="112.334" height="0.516129" fill="white" />
<path
d="M66.3454 77.5155H66.0366L65.3992 77.9388V78.2525L66.0217 77.8392H66.0366V80.0652H66.3454V77.5155Z"
fill="#9BA6B6"
/>
<path
d="M81.2501 80.0652H82.8586V79.7913H81.6734V79.7714L82.2461 79.1588C82.6843 78.6895 82.8138 78.4704 82.8138 78.1877C82.8138 77.7943 82.4951 77.4806 82.0469 77.4806C81.6 77.4806 81.2601 77.7844 81.2601 78.2326H81.5539C81.5539 77.9425 81.7419 77.7495 82.0369 77.7495C82.3133 77.7495 82.525 77.9188 82.525 78.1877C82.525 78.4231 82.3868 78.5973 82.0917 78.9198L81.2501 79.8411V80.0652Z"
fill="#9BA6B6"
/>
<path
d="M98.1047 80.1C98.599 80.1 98.9662 79.79 98.9662 79.373C98.9662 79.0493 98.7745 78.814 98.4533 78.7604V78.7405C98.711 78.6621 98.8716 78.4504 98.8716 78.1628C98.8716 77.8018 98.5865 77.4806 98.1147 77.4806C97.6739 77.4806 97.3079 77.752 97.293 78.1529H97.5918C97.603 77.8989 97.8445 77.7495 98.1097 77.7495C98.3911 77.7495 98.5728 77.9201 98.5728 78.1778C98.5728 78.4467 98.3624 78.621 98.0599 78.621H97.8557V78.8949H98.0599C98.4471 78.8949 98.6625 79.0916 98.6625 79.373C98.6625 79.6431 98.4272 79.8261 98.0997 79.8261C97.8047 79.8261 97.5706 79.6743 97.5519 79.4278H97.2382C97.2569 79.8286 97.6105 80.1 98.1047 80.1Z"
fill="#9BA6B6"
/>
<path
d="M113.222 79.5423H114.423V80.0652H114.716V79.5423H115.065V79.2684H114.716V77.5155H114.343L113.222 79.2883V79.5423ZM114.423 79.2684H113.556V79.2485L114.403 77.9089H114.423V79.2684Z"
fill="#9BA6B6"
/>
<path
d="M17.8508 96.1477C18.3364 96.1477 18.6925 95.7892 18.6925 95.3011C18.6925 94.8069 18.3488 94.4446 17.8807 94.4446C17.7089 94.4446 17.5421 94.5056 17.4375 94.589H17.4226L17.5122 93.837H18.5779V93.5631H17.2533L17.0989 94.8181L17.3877 94.8529C17.4935 94.777 17.6741 94.7222 17.8309 94.7235C18.1559 94.7259 18.3937 94.9724 18.3937 95.3061C18.3937 95.6335 18.1646 95.8738 17.8508 95.8738C17.5894 95.8738 17.3815 95.7057 17.3578 95.4754H17.059C17.0777 95.8639 17.4126 96.1477 17.8508 96.1477Z"
fill="#9BA6B6"
/>
<rect x="26.3188" y="87.2924" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M33.9284 96.1478C34.4786 96.1516 34.8484 95.7731 34.8472 95.2689C34.8484 94.7871 34.5048 94.4385 34.0578 94.4385C33.7839 94.4385 33.5424 94.5717 33.4204 94.7908H33.403C33.4042 94.2542 33.6009 93.928 33.9545 93.928C34.1736 93.928 34.3218 94.055 34.3691 94.2505H34.8235C34.7687 93.8384 34.4363 93.5284 33.9545 93.5284C33.342 93.5284 32.9548 94.0388 32.9548 94.9103C32.9535 95.8453 33.4391 96.1453 33.9284 96.1478ZM33.9259 95.7743C33.6532 95.7743 33.454 95.549 33.4528 95.2826C33.4553 95.0149 33.6619 94.7908 33.9321 94.7908C34.2023 94.7908 34.4002 95.0049 34.399 95.2788C34.4002 95.5577 34.196 95.7743 33.9259 95.7743Z"
fill="#3B82F6"
/>
<circle cx="34.1386" cy="99.9637" r="0.657352" fill="#3B82F6" />
<rect x="42.3665" y="87.2924" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M49.2434 96.113H49.7228L50.8059 93.9579V93.5632H49.0691V93.9492H50.3278V93.9666L49.2434 96.113Z"
fill="#3B82F6"
/>
<rect x="58.4143" y="87.2924" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M66.0013 96.1478C66.5528 96.1478 66.9475 95.8441 66.9487 95.4295C66.9475 95.1108 66.7122 94.8443 66.4159 94.7945V94.7771C66.6736 94.7198 66.8529 94.4883 66.8541 94.2119C66.8529 93.8197 66.4918 93.5284 66.0013 93.5284C65.5071 93.5284 65.146 93.8185 65.1473 94.2119C65.146 94.4883 65.3228 94.7198 65.5855 94.7771V94.7945C65.2842 94.8443 65.0514 95.1108 65.0526 95.4295C65.0514 95.8441 65.4448 96.1478 66.0013 96.1478ZM66.0013 95.7918C65.7125 95.7918 65.5257 95.6324 65.5282 95.3971C65.5257 95.1531 65.7262 94.98 66.0013 94.98C66.2727 94.98 66.4719 95.1543 66.4744 95.3971C66.4719 95.6324 66.2864 95.7918 66.0013 95.7918ZM66.0013 94.6302C65.7648 94.6302 65.5955 94.4771 65.5979 94.2555C65.5955 94.0363 65.7598 93.8894 66.0013 93.8894C66.2391 93.8894 66.4022 94.0363 66.4047 94.2555C66.4022 94.4783 66.2341 94.6302 66.0013 94.6302Z"
fill="#3B82F6"
/>
<rect x="74.4617" y="87.2924" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M82.0226 93.5284C81.4698 93.5247 81.1026 93.9044 81.1026 94.4024C81.1038 94.8829 81.4462 95.2303 81.8931 95.2303C82.1683 95.2303 82.4073 95.0971 82.5306 94.878H82.548C82.5468 95.4233 82.3488 95.7482 81.9965 95.7482C81.7761 95.7482 81.628 95.6212 81.5819 95.4183H81.1275C81.1798 95.8403 81.5134 96.1478 81.9965 96.1478C82.6078 96.1478 82.9974 95.6374 82.9962 94.7597C82.995 93.8309 82.5119 93.5309 82.0226 93.5284ZM82.0239 93.9019C82.2965 93.9019 82.497 94.1285 82.497 94.3887C82.4982 94.6526 82.2878 94.8792 82.0189 94.8792C81.7475 94.8792 81.552 94.6651 81.5508 94.3924C81.5508 94.1185 81.7537 93.9019 82.0239 93.9019Z"
fill="#3B82F6"
/>
<rect x="90.5098" y="87.2924" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M97.3476 93.5632H96.9081L96.2744 93.9704V94.3937L96.8708 94.0127H96.8857V96.113H97.3476V93.5632ZM98.9375 96.1615C99.5525 96.1628 99.9197 95.6772 99.9197 94.8406C99.9197 94.009 99.55 93.5284 98.9375 93.5284C98.3249 93.5284 97.9564 94.0077 97.9552 94.8406C97.9552 95.676 98.3224 96.1615 98.9375 96.1615ZM98.9375 95.7719C98.62 95.7719 98.4208 95.4531 98.422 94.8406C98.4233 94.233 98.6212 93.9131 98.9375 93.9131C99.2549 93.9131 99.4529 94.233 99.4541 94.8406C99.4541 95.4531 99.2562 95.7719 98.9375 95.7719Z"
fill="#3B82F6"
/>
<path
d="M113.674 93.5632H113.365L112.727 93.9865V94.3003L113.35 93.8869H113.365V96.113H113.674V93.5632ZM115.303 93.5632H114.995L114.357 93.9865V94.3003L114.98 93.8869H114.995V96.113H115.303V93.5632Z"
fill="#9BA6B6"
/>
<rect x="10.2712" y="103.34" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M17.1895 109.611H16.7501L16.1164 110.018V110.441L16.7127 110.06H16.7276V112.161H17.1895V109.611ZM17.8369 112.161H19.5849V111.775H18.4744V111.757L18.9138 111.31C19.4093 110.835 19.5463 110.603 19.5463 110.316C19.5463 109.889 19.1989 109.576 18.686 109.576C18.1805 109.576 17.822 109.89 17.822 110.374H18.2615C18.2615 110.114 18.4258 109.951 18.6798 109.951C18.9226 109.951 19.1031 110.099 19.1031 110.339C19.1031 110.552 18.9736 110.704 18.7221 110.959L17.8369 111.827V112.161Z"
fill="#3B82F6"
/>
<rect x="26.3188" y="103.34" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M33.1843 109.611H32.7448L32.1111 110.018V110.441L32.7075 110.06H32.7224V112.161H33.1843V109.611ZM34.7468 112.196C35.2921 112.196 35.6892 111.883 35.688 111.452C35.6892 111.134 35.49 110.905 35.1327 110.853V110.834C35.4091 110.774 35.5946 110.568 35.5934 110.282C35.5946 109.894 35.2634 109.576 34.7542 109.576C34.2587 109.576 33.8753 109.871 33.8653 110.298H34.3098C34.3173 110.084 34.5165 109.951 34.7518 109.951C34.9895 109.951 35.1477 110.095 35.1464 110.309C35.1477 110.532 34.9634 110.68 34.6995 110.68H34.4741V111.036H34.6995C35.0219 111.036 35.2136 111.198 35.2124 111.429C35.2136 111.654 35.0182 111.808 34.7455 111.808C34.4891 111.808 34.2911 111.675 34.2799 111.467H33.8118C33.8242 111.898 34.2089 112.196 34.7468 112.196Z"
fill="#3B82F6"
/>
<rect x="42.3665" y="103.34" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M49.2063 109.611H48.7668L48.1331 110.018V110.441L48.7294 110.06H48.7444V112.161H49.2063V109.611ZM49.8076 111.688H51.0239V112.161H51.4647V111.688H51.7908V111.308H51.4647V109.611H50.8895L49.8076 111.32V111.688ZM51.0289 111.308H50.2807V111.288L51.009 110.134H51.0289V111.308Z"
fill="#3B82F6"
/>
<path
d="M65.2789 109.611H64.9702L64.3327 110.034V110.348L64.9552 109.935H64.9702V112.161H65.2789V109.611ZM66.809 112.196C67.2945 112.196 67.6506 111.837 67.6506 111.349C67.6506 110.855 67.307 110.492 66.8389 110.492C66.6671 110.492 66.5002 110.553 66.3957 110.637H66.3807L66.4704 109.885H67.5361V109.611H66.2114L66.057 110.866L66.3459 110.901C66.4517 110.825 66.6322 110.77 66.7891 110.771C67.114 110.774 67.3518 111.02 67.3518 111.354C67.3518 111.681 67.1227 111.922 66.809 111.922C66.5476 111.922 66.3396 111.754 66.316 111.523H66.0172C66.0359 111.912 66.3708 112.196 66.809 112.196Z"
fill="#9BA6B6"
/>
<rect x="74.4617" y="103.34" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M81.3323 109.611H80.8928L80.2591 110.018V110.441L80.8554 110.06H80.8704V112.161H81.3323V109.611ZM82.9134 112.196C83.4637 112.199 83.8335 111.821 83.8322 111.317C83.8335 110.835 83.4898 110.486 83.0429 110.486C82.769 110.486 82.5275 110.619 82.4055 110.839H82.388C82.3893 110.302 82.586 109.976 82.9396 109.976C83.1587 109.976 83.3068 110.103 83.3541 110.298H83.8086C83.7538 109.886 83.4214 109.576 82.9396 109.576C82.327 109.576 81.9398 110.087 81.9398 110.958C81.9386 111.893 82.4241 112.193 82.9134 112.196ZM82.9109 111.822C82.6383 111.822 82.4391 111.597 82.4378 111.33C82.4403 111.063 82.647 110.839 82.9171 110.839C83.1873 110.839 83.3853 111.053 83.384 111.327C83.3853 111.605 83.1811 111.822 82.9109 111.822Z"
fill="#3B82F6"
/>
<path
d="M97.4394 109.611H97.1307L96.4932 110.034V110.348L97.1157 109.935H97.1307V112.161H97.4394V109.611ZM98.2524 112.161H98.5761L99.7115 109.9V109.611H98.0781V109.885H99.3928V109.905L98.2524 112.161Z"
fill="#9BA6B6"
/>
<path
d="M113.408 109.611H113.1L112.462 110.034V110.348L113.085 109.935H113.1V112.161H113.408V109.611ZM114.958 112.196C115.467 112.196 115.822 111.898 115.825 111.483C115.822 111.161 115.607 110.887 115.332 110.836V110.821C115.571 110.759 115.728 110.525 115.73 110.253C115.728 109.865 115.402 109.576 114.958 109.576C114.51 109.576 114.184 109.865 114.186 110.253C114.184 110.525 114.341 110.759 114.585 110.821V110.836C114.305 110.887 114.089 111.161 114.092 111.483C114.089 111.898 114.444 112.196 114.958 112.196ZM114.958 111.922C114.608 111.922 114.393 111.742 114.396 111.469C114.393 111.181 114.631 110.976 114.958 110.976C115.281 110.976 115.519 111.181 115.521 111.469C115.519 111.742 115.303 111.922 114.958 111.922ZM114.958 110.712C114.679 110.712 114.483 110.537 114.485 110.273C114.483 110.014 114.672 109.845 114.958 109.845C115.24 109.845 115.429 110.014 115.431 110.273C115.429 110.537 115.232 110.712 114.958 110.712Z"
fill="#9BA6B6"
/>
<rect x="10.2712" y="119.388" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M17.1416 125.659H16.7021L16.0684 126.066V126.489L16.6648 126.108H16.6797V128.208H17.1416V125.659ZM18.6742 125.624C18.1214 125.62 17.7541 126 17.7541 126.498C17.7554 126.978 18.0978 127.326 18.5447 127.326C18.8198 127.326 19.0589 127.192 19.1821 126.973H19.1996C19.1983 127.519 19.0004 127.844 18.648 127.844C18.4277 127.844 18.2795 127.717 18.2335 127.514H17.779C17.8313 127.936 18.165 128.243 18.648 128.243C19.2593 128.243 19.649 127.733 19.6478 126.855C19.6465 125.926 19.1635 125.626 18.6742 125.624ZM18.6754 125.997C18.9481 125.997 19.1485 126.224 19.1485 126.484C19.1498 126.748 18.9394 126.975 18.6704 126.975C18.399 126.975 18.2036 126.76 18.2023 126.488C18.2023 126.214 18.4053 125.997 18.6754 125.997Z"
fill="#3B82F6"
/>
<rect x="26.3188" y="119.388" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M31.8734 128.208H33.6213V127.822H32.5108V127.805L32.9503 127.358C33.4458 126.882 33.5827 126.651 33.5827 126.363C33.5827 125.936 33.2354 125.624 32.7224 125.624C32.217 125.624 31.8584 125.937 31.8584 126.422H32.2979C32.2979 126.162 32.4622 125.998 32.7162 125.998C32.959 125.998 33.1395 126.147 33.1395 126.387C33.1395 126.6 33.01 126.752 32.7585 127.007L31.8734 127.875V128.208ZM34.9933 128.257C35.6083 128.258 35.9756 127.773 35.9756 126.936C35.9756 126.104 35.6058 125.624 34.9933 125.624C34.3808 125.624 34.0122 126.103 34.011 126.936C34.011 127.771 34.3783 128.257 34.9933 128.257ZM34.9933 127.867C34.6758 127.867 34.4766 127.548 34.4779 126.936C34.4791 126.328 34.6771 126.008 34.9933 126.008C35.3108 126.008 35.5087 126.328 35.51 126.936C35.51 127.548 35.312 127.867 34.9933 127.867Z"
fill="#3B82F6"
/>
<rect x="42.3665" y="119.388" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M48.2479 128.208H49.9959V127.822H48.8854V127.805L49.3248 127.358C49.8203 126.882 49.9573 126.651 49.9573 126.363C49.9573 125.936 49.6099 125.624 49.097 125.624C48.5915 125.624 48.233 125.937 48.233 126.422H48.6725C48.6725 126.162 48.8368 125.998 49.0908 125.998C49.3335 125.998 49.5141 126.147 49.5141 126.387C49.5141 126.6 49.3846 126.752 49.1331 127.007L48.2479 127.875V128.208ZM51.4625 125.659H51.023L50.3893 126.066V126.489L50.9856 126.108H51.0006V128.208H51.4625V125.659Z"
fill="#3B82F6"
/>
<rect x="58.4143" y="119.388" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M64.049 128.208H65.797V127.822H64.6865V127.805L65.1259 127.358C65.6214 126.882 65.7584 126.651 65.7584 126.363C65.7584 125.936 65.411 125.624 64.8981 125.624C64.3926 125.624 64.0341 125.937 64.0341 126.422H64.4736C64.4736 126.162 64.6379 125.998 64.8919 125.998C65.1347 125.998 65.3152 126.147 65.3152 126.387C65.3152 126.6 65.1857 126.752 64.9342 127.007L64.049 127.875V128.208ZM66.2265 128.208H67.9745V127.822H66.8639V127.805L67.3034 127.358C67.7989 126.882 67.9359 126.651 67.9359 126.363C67.9359 125.936 67.5885 125.624 67.0756 125.624C66.5701 125.624 66.2116 125.937 66.2116 126.422H66.651C66.651 126.162 66.8154 125.998 67.0694 125.998C67.3121 125.998 67.4927 126.147 67.4927 126.387C67.4927 126.6 67.3632 126.752 67.1117 127.007L66.2265 127.875V128.208Z"
fill="#3B82F6"
/>
<rect x="74.4617" y="119.388" width="15.1717" height="15.1717" fill="#DBEAFE" />
<path
d="M80.0436 128.208H81.7915V127.822H80.681V127.805L81.1205 127.358C81.616 126.882 81.7529 126.651 81.7529 126.363C81.7529 125.936 81.4056 125.624 80.8926 125.624C80.3872 125.624 80.0286 125.937 80.0286 126.422H80.4681C80.4681 126.162 80.6324 125.998 80.8864 125.998C81.1292 125.998 81.3097 126.147 81.3097 126.387C81.3097 126.6 81.1802 126.752 80.9287 127.007L80.0436 127.875V128.208ZM83.1361 128.243C83.6814 128.243 84.0786 127.931 84.0773 127.5C84.0786 127.181 83.8794 126.952 83.5221 126.901V126.881C83.7984 126.821 83.9839 126.616 83.9827 126.33C83.9839 125.941 83.6528 125.624 83.1436 125.624C82.6481 125.624 82.2646 125.919 82.2547 126.346H82.6991C82.7066 126.132 82.9058 125.998 83.1411 125.998C83.3789 125.998 83.537 126.143 83.5357 126.357C83.537 126.58 83.3527 126.728 83.0888 126.728H82.8635V127.084H83.0888C83.4112 127.084 83.603 127.246 83.6017 127.476C83.603 127.702 83.4075 127.856 83.1349 127.856C82.8784 127.856 82.6804 127.723 82.6692 127.515H82.2011C82.2136 127.946 82.5983 128.243 83.1361 128.243Z"
fill="#3B82F6"
/>
<path
d="M96.2007 128.208H97.8092V127.934H96.624V127.914L97.1967 127.302C97.6349 126.833 97.7644 126.613 97.7644 126.331C97.7644 125.937 97.4456 125.624 96.9975 125.624C96.5505 125.624 96.2106 125.928 96.2106 126.376H96.5044C96.5044 126.086 96.6924 125.893 96.9875 125.893C97.2639 125.893 97.4755 126.062 97.4755 126.331C97.4755 126.566 97.3373 126.74 97.0423 127.063L96.2007 127.984V128.208ZM98.2088 127.685H99.409V128.208H99.7028V127.685H100.051V127.412H99.7028V125.659H99.3293L98.2088 127.431V127.685ZM99.409 127.412H98.5425V127.392L99.3891 126.052H99.409V127.412Z"
fill="#9BA6B6"
/>
<path
d="M112.279 128.208H113.888V127.934H112.702V127.914L113.275 127.302C113.713 126.833 113.843 126.613 113.843 126.331C113.843 125.937 113.524 125.624 113.076 125.624C112.629 125.624 112.289 125.928 112.289 126.376H112.583C112.583 126.086 112.771 125.893 113.066 125.893C113.342 125.893 113.554 126.062 113.554 126.331C113.554 126.566 113.416 126.74 113.121 127.063L112.279 127.984V128.208ZM115.199 128.243C115.684 128.243 116.04 127.885 116.04 127.397C116.04 126.902 115.697 126.54 115.228 126.54C115.057 126.54 114.89 126.601 114.785 126.684H114.77L114.86 125.932H115.926V125.659H114.601L114.447 126.914L114.735 126.948C114.841 126.872 115.022 126.818 115.179 126.819C115.504 126.821 115.741 127.068 115.741 127.402C115.741 127.729 115.512 127.969 115.199 127.969C114.937 127.969 114.729 127.801 114.706 127.571H114.407C114.425 127.959 114.76 128.243 115.199 128.243Z"
fill="#9BA6B6"
/>
</g>
</g>
<g clipPath="url(#clip0)">
<path
d="M54.1289 22.6026C54.1289 16.0129 59.4709 10.671 66.0605 10.671C72.6502 10.671 77.9922 16.0129 77.9922 22.6026C77.9922 29.1923 72.6502 34.5342 66.0605 34.5342C59.4709 34.5342 54.1289 29.1923 54.1289 22.6026Z"
fill="#F4F5F6"
/>
<path
d="M77.7885 31.2806C77.9226 31.4509 77.9922 31.6625 77.9922 31.8793V33.5352C77.9922 34.0875 77.5445 34.5352 76.9922 34.5352H55.1289C54.5766 34.5352 54.1289 34.0875 54.1289 33.5352V31.889C54.1289 31.673 54.198 31.4621 54.3312 31.2921C55.6901 29.5582 57.4178 28.1462 59.3905 27.1595C61.4626 26.1232 63.7478 25.5845 66.0645 25.5865C70.8206 25.5865 75.0583 27.8133 77.7885 31.2806ZM70.0397 19.6197C70.0397 20.6745 69.6207 21.6861 68.8748 22.432C68.129 23.1779 67.1173 23.5969 66.0625 23.5969C65.0077 23.5969 63.9961 23.1779 63.2502 22.432C62.5043 21.6861 62.0853 20.6745 62.0853 19.6197C62.0853 18.5648 62.5043 17.5532 63.2502 16.8074C63.9961 16.0615 65.0077 15.6425 66.0625 15.6425C67.1173 15.6425 68.129 16.0615 68.8748 16.8074C69.6207 17.5532 70.0397 18.5648 70.0397 19.6197Z"
fill="#708097"
/>
</g>
<defs>
<filter
id="filter0_dd"
x="5.49919"
y="53.0038"
width="121.001"
height="88.3916"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="0.516129"
operator="erode"
in="SourceAlpha"
result="effect1_dropShadow"
/>
<feOffset dy="1.03226" />
<feGaussianBlur stdDeviation="1.03226" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="0.516129"
operator="erode"
in="SourceAlpha"
result="effect2_dropShadow"
/>
<feOffset dy="2.06452" />
<feGaussianBlur stdDeviation="1.54839" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape" />
</filter>
<clipPath id="clip0">
<path
d="M54.1289 22.6026C54.1289 16.0129 59.4709 10.671 66.0605 10.671C72.6502 10.671 77.9922 16.0129 77.9922 22.6026C77.9922 29.1923 72.6502 34.5342 66.0605 34.5342C59.4709 34.5342 54.1289 29.1923 54.1289 22.6026Z"
fill="white"
/>
</clipPath>
</defs>
</svg>
<h3 className="mt-2 text-xl font-bold text-neutral-900">Create your first event type</h3>
<p className="mt-1 text-md text-neutral-600">
Event types enable you to share links that show available times on your calendar and allow
people to make bookings with you.
</p>
</div>
)}
{showAddModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="mb-4">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Add a new event type
</h3>
<div>
<p className="text-sm text-gray-500">
Create a new event type for people to book times with.
</p>
</div>
</div>
<form onSubmit={createEventTypeHandler}>
<div>
<div className="mb-4">
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<div className="mt-1">
<input
ref={titleRef}
type="text"
name="title"
id="title"
required
className="shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Quick Chat"
/>
</div>
</div>
<div className="mb-4">
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">
URL
</label>
<div className="mt-1">
<div className="flex rounded-sm shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
{location.hostname}/{user.username}/
</span>
<input
ref={slugRef}
type="text"
name="slug"
id="slug"
required
className="flex-1 block w-full focus:ring-neutral-900 focus:border-neutral-900 min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
/>
</div>
</div>
</div>
<div className="mb-4">
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
</label>
<div className="mt-1">
<textarea
ref={descriptionRef}
name="description"
id="description"
className="shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="A quick video meeting."></textarea>
</div>
</div>
<div className="mb-4">
<label htmlFor="length" className="block text-sm font-medium text-gray-700">
Length
</label>
<div className="mt-1 relative rounded-sm shadow-sm">
<input
ref={lengthRef}
type="number"
name="length"
id="length"
required
className="focus:ring-neutral-900 focus:border-neutral-900 block w-full pr-20 sm:text-sm border-gray-300 rounded-sm"
placeholder="15"
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 text-sm">
minutes
</div>
</div>
</div>
</div>
{/* TODO: Add an error message when required input fields empty*/}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
Create
</button>
<button onClick={toggleAddModal} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
)}
</Shell>
</div>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
startTime: true,
endTime: true,
bufferTime: true,
},
});
const types = await prisma.eventType.findMany({
where: {
userId: user.id,
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true,
},
});
return {
props: { user, types }, // will be passed to the page component as props
};
}

View File

@ -1,130 +1,131 @@
import Head from 'next/head';
import prisma from '../../lib/prisma';
import { getIntegrationName, getIntegrationType } from '../../lib/integrations';
import Shell from '../../components/Shell';
import { useState } from 'react';
import { useRouter } from 'next/router';
import { useSession, getSession } from 'next-auth/client';
import Head from "next/head";
import prisma from "../../lib/prisma";
import { getIntegrationName, getIntegrationType } from "../../lib/integrations";
import Shell from "../../components/Shell";
import { useState } from "react";
import { useRouter } from "next/router";
import { useSession, getSession } from "next-auth/client";
export default function integration(props) {
const router = useRouter();
const [session, loading] = useSession();
const [showAPIKey, setShowAPIKey] = useState(false);
const router = useRouter();
const [session, loading] = useSession();
const [showAPIKey, setShowAPIKey] = useState(false);
if (loading) {
return <div className="loader"></div>;
}
if (loading) {
return <div className="loader"></div>;
}
function toggleShowAPIKey() {
setShowAPIKey(!showAPIKey);
}
function toggleShowAPIKey() {
setShowAPIKey(!showAPIKey);
}
async function deleteIntegrationHandler(event) {
event.preventDefault();
async function deleteIntegrationHandler(event) {
event.preventDefault();
const response = await fetch('/api/integrations', {
method: 'DELETE',
body: JSON.stringify({id: props.integration.id}),
headers: {
'Content-Type': 'application/json'
}
});
const response = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: props.integration.id }),
headers: {
"Content-Type": "application/json",
},
});
router.push('/integrations');
}
router.push("/integrations");
}
return(
<div>
<Head>
<title>{getIntegrationName(props.integration.type)} | Integrations | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
return (
<div>
<Head>
<title>{getIntegrationName(props.integration.type)} | Integrations | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading={getIntegrationName(props.integration.type)}>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 bg-white shadow overflow-hidden rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Integration Details
</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Information about your {getIntegrationName(props.integration.type)} integration.
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<dl className="grid gap-y-8">
<div>
<dt className="text-sm font-medium text-gray-500">
Integration name
</dt>
<dd className="mt-1 text-sm text-gray-900">
{getIntegrationName(props.integration.type)}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">
Integration type
</dt>
<dd className="mt-1 text-sm text-gray-900">
{getIntegrationType(props.integration.type)}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">
API Key
</dt>
<dd className="mt-1 text-sm text-gray-900">
{!showAPIKey ?
<span>&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;</span>
:
<div>
<textarea name="apikey" rows={6} className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" readOnly>{JSON.stringify(props.integration.key)}</textarea>
</div>}
<button onClick={toggleShowAPIKey} className="ml-2 font-medium text-blue-600 hover:text-blue-700">{!showAPIKey ? 'Show' : 'Hide'}</button>
</dd>
</div>
</dl>
</div>
</div>
<div>
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete this integration
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Once you delete this integration, it will be permanently removed.
</p>
</div>
<div className="mt-5">
<button onClick={deleteIntegrationHandler} type="button" className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm">
Delete integration
</button>
</div>
</div>
</div>
</div>
</div>
</Shell>
<Shell heading={getIntegrationName(props.integration.type)}>
<div className="flex mb-8">
<p className="text-sm text-neutral-500">Manage and delete integrations.</p>
</div>
);
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 bg-white shadow overflow-hidden rounded-sm">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Integration Details</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Information about your {getIntegrationName(props.integration.type)} integration.
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<dl className="grid gap-y-8">
<div>
<dt className="text-sm font-medium text-gray-500">Integration name</dt>
<dd className="mt-1 text-sm text-gray-900">{getIntegrationName(props.integration.type)}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Integration type</dt>
<dd className="mt-1 text-sm text-gray-900">{getIntegrationType(props.integration.type)}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">API Key</dt>
<dd className="mt-1 text-sm text-gray-900">
{!showAPIKey ? (
<span>&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;</span>
) : (
<div>
<textarea
name="apikey"
rows={6}
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 block w-full sm:text-sm border-gray-300 rounded-sm"
readOnly>
{JSON.stringify(props.integration.key)}
</textarea>
</div>
)}
<button
onClick={toggleShowAPIKey}
className="ml-2 font-medium text-neutral-900 hover:text-neutral-700">
{!showAPIKey ? "Show" : "Hide"}
</button>
</dd>
</div>
</dl>
</div>
</div>
<div>
<div className="bg-white shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete this integration</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>Once you delete this integration, it will be permanently removed.</p>
</div>
<div className="mt-5">
<button
onClick={deleteIntegrationHandler}
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-sm text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm">
Delete integration
</button>
</div>
</div>
</div>
</div>
</div>
</Shell>
</div>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
const session = await getSession(context);
const integration = await prisma.credential.findFirst({
where: {
id: parseInt(context.query.integration),
},
select: {
id: true,
type: true,
key: true
}
});
return {
props: {integration}, // will be passed to the page component as props
}
}
const integration = await prisma.credential.findFirst({
where: {
id: parseInt(context.query.integration),
},
select: {
id: true,
type: true,
key: true,
},
});
return {
props: { integration }, // will be passed to the page component as props
};
}

View File

@ -1,387 +1,461 @@
import Head from 'next/head';
import Link from 'next/link';
import prisma from '../../lib/prisma';
import Shell from '../../components/Shell';
import {useEffect, useState} from 'react';
import {getSession, useSession} from 'next-auth/client';
import {CalendarIcon, CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid';
import {InformationCircleIcon} from '@heroicons/react/outline';
import {Switch} from '@headlessui/react'
import Head from "next/head";
import Link from "next/link";
import prisma from "../../lib/prisma";
import Shell from "../../components/Shell";
import { useEffect, useState } from "react";
import { getSession, useSession } from "next-auth/client";
import {
CalendarIcon,
CheckCircleIcon,
ChevronRightIcon,
PlusIcon,
XCircleIcon,
} from "@heroicons/react/solid";
import { InformationCircleIcon } from "@heroicons/react/outline";
import { Switch } from "@headlessui/react";
export default function Home({ integrations }) {
const [session, loading] = useSession();
const [showAddModal, setShowAddModal] = useState(false);
const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false);
const [selectableCalendars, setSelectableCalendars] = useState([]);
const [session, loading] = useSession();
const [showAddModal, setShowAddModal] = useState(false);
const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false);
const [selectableCalendars, setSelectableCalendars] = useState([]);
function toggleAddModal() {
setShowAddModal(!showAddModal);
function toggleAddModal() {
setShowAddModal(!showAddModal);
}
function toggleShowCalendarModal() {
setShowSelectCalendarModal(!showSelectCalendarModal);
}
function loadCalendars() {
fetch("api/availability/calendar")
.then((response) => response.json())
.then((data) => {
setSelectableCalendars(data);
});
}
function integrationHandler(type) {
fetch("/api/integrations/" + type.replace("_", "") + "/add")
.then((response) => response.json())
.then((data) => (window.location.href = data.url));
}
function calendarSelectionHandler(calendar) {
return (selected) => {
const cals = [...selectableCalendars];
const i = cals.findIndex((c) => c.externalId === calendar.externalId);
cals[i].selected = selected;
setSelectableCalendars(cals);
if (selected) {
fetch("api/availability/calendar", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(cals[i]),
}).then((response) => response.json());
} else {
fetch("api/availability/calendar", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(cals[i]),
}).then((response) => response.json());
}
};
}
function getCalendarIntegrationImage(integrationType: string) {
switch (integrationType) {
case "google_calendar":
return "integrations/google-calendar.png";
case "office365_calendar":
return "integrations/office-365.png";
default:
return "";
}
}
function toggleShowCalendarModal() {
setShowSelectCalendarModal(!showSelectCalendarModal);
}
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
function loadCalendars() {
fetch('api/availability/calendar')
.then((response) => response.json())
.then(data => {
setSelectableCalendars(data)
});
}
useEffect(loadCalendars, [integrations]);
function integrationHandler(type) {
fetch('/api/integrations/' + type.replace('_', '') + '/add')
.then((response) => response.json())
.then((data) => window.location.href = data.url);
}
if (loading) {
return <div className="loader"></div>;
}
function calendarSelectionHandler(calendar) {
return (selected) => {
let cals = [...selectableCalendars];
let i = cals.findIndex(c => c.externalId === calendar.externalId);
cals[i].selected = selected;
setSelectableCalendars(cals);
if (selected) {
fetch('api/availability/calendar', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cals[i])
}).then((response) => response.json());
} else {
fetch('api/availability/calendar', {
method: 'DELETE', headers: {
'Content-Type': 'application/json'
}, body: JSON.stringify(cals[i])
}).then((response) => response.json());
}
}
}
return (
<div>
<Head>
<title>Integrations | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
function getCalendarIntegrationImage(integrationType: string){
switch (integrationType) {
case "google_calendar": return "integrations/google-calendar.png";
case "office365_calendar": return "integrations/office-365.png";
default: return "";
}
}
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
useEffect(loadCalendars, [integrations]);
if (loading) {
return <div className="loader"></div>;
}
return (
<div>
<Head>
<title>Integrations | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading="Integrations" noPaddingBottom>
<div className="text-right py-2">
<button onClick={toggleAddModal} type="button"
className="px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Add new integration
</button>
</div>
<div className="bg-white shadow overflow-hidden rounded-lg mb-8">
{integrations.filter( (ig) => ig.credential ).length !== 0 ? <ul className="divide-y divide-gray-200">
{integrations.filter(ig => ig.credential).map( (ig) => (<li key={ig.id}>
<Link href={"/integrations/" + ig.credential.id}>
<a className="block hover:bg-gray-50">
<div className="flex items-center px-4 py-4 sm:px-6">
<div className="min-w-0 flex-1 flex items-center">
<div className="flex-shrink-0">
<img className="h-10 w-10 mr-2" src={ig.imageSrc} alt={ig.title} />
</div>
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
<div>
<p className="text-sm font-medium text-blue-600 truncate">{ig.title}</p>
<p className="flex items-center text-sm text-gray-500">
{ig.type.endsWith('_calendar') && <span className="truncate">Calendar Integration</span>}
{ig.type.endsWith('_video') && <span className="truncate">Video Conferencing</span>}
</p>
</div>
<div className="hidden md:block">
{ig.credential.key && <p className="mt-2 flex items-center text text-gray-500">
<CheckCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-green-400" />
Connected
</p>}
{!ig.credential.key && <p className="mt-3 flex items-center text text-gray-500">
<XCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-yellow-400" />
Not connected
</p>}
</div>
</div>
<div>
<ChevronRightIcon className="h-5 w-5 text-gray-400" />
</div>
</div>
</div>
</a>
</Link>
</li>))}
</ul>
:
<div className="bg-white shadow rounded-lg">
<div className="flex">
<div className="py-9 pl-8">
<InformationCircleIcon className="text-blue-600 w-16" />
</div>
<div className="py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
You don&apos;t have any integrations added.
</h3>
<div className="mt-2 text-sm text-gray-500">
<p>
You currently do not have any integrations set up. Add your first integration to get started.
</p>
</div>
<div className="mt-3 text-sm">
<button onClick={toggleAddModal} className="font-medium text-blue-600 hover:text-blue-500"> Add your first integration <span aria-hidden="true">&rarr;</span></button>
</div>
</div>
</div>
</div>
}
</div>
{showAddModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="h-6 w-6 text-blue-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Add a new integration
</h3>
<div>
<p className="text-sm text-gray-400">
Link a new integration to your account.
</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{integrations.filter( (integration) => integration.installed ).map( (integration) => (<li key={integration.type} className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img className="h-8 w-8 mr-2" src={integration.imageSrc} alt={integration.title} />
</div>
<div className="w-10/12">
<h2 className="text-gray-800 font-medium">{ integration.title }</h2>
<p className="text-gray-400 text-sm">{ integration.description }</p>
</div>
<div className="w-2/12 text-right pt-2">
<button onClick={() => integrationHandler(integration.type)} className="font-medium text-blue-600 hover:text-blue-500">Add</button>
</div>
</li>))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button onClick={toggleAddModal} type="button" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Select calendars
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>
Select which calendars are checked for availability to prevent double bookings.
</p>
</div>
<div className="mt-5">
<button type="button" onClick={toggleShowCalendarModal} className="btn btn-primary">
Select calendars
</button>
</div>
</div>
</div>
{showSelectCalendarModal &&
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<CalendarIcon className="h-6 w-6 text-blue-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Select calendars
</h3>
<div>
<p className="text-sm text-gray-400">
If no entry is selected, all calendars will be checked
</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{selectableCalendars.map( (calendar) => (<li key={calendar.name} className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img className="h-8 w-8 mr-2" src={getCalendarIntegrationImage(calendar.integration)} alt={calendar.integration} />
</div>
<div className="w-10/12 pt-3">
<h2 className="text-gray-800 font-medium">{ calendar.name }</h2>
</div>
<div className="w-2/12 text-right pt-3">
<Switch
checked={calendar.selected}
onChange={calendarSelectionHandler(calendar)}
className={classNames(
calendar.selected ? 'bg-indigo-600' : 'bg-gray-200',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
)}
>
<span className="sr-only">Select calendar</span>
<span
aria-hidden="true"
className={classNames(
calendar.selected ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
</div>
</li>))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button onClick={toggleShowCalendarModal} type="button" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
}
</Shell>
<Shell heading="Integrations" noPaddingBottom>
<div className="flex">
<p className="text-sm text-neutral-500">Connect your favourite apps.</p>
</div>
);
<div className="text-right py-2">
<button
onClick={toggleAddModal}
type="button"
className="absolute top-8 right-8 flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
<PlusIcon className="w-5 h-5 mr-1" />
Add new integration
</button>
</div>
<div className="bg-white shadow overflow-hidden rounded-sm mb-8">
{integrations.filter((ig) => ig.credential).length !== 0 ? (
<ul className="divide-y divide-gray-200">
{integrations
.filter((ig) => ig.credential)
.map((ig) => (
<li key={ig.id}>
<Link href={"/integrations/" + ig.credential.id}>
<a className="block hover:bg-gray-50">
<div className="flex items-center px-4 py-4 sm:px-6">
<div className="min-w-0 flex-1 flex items-center">
<div className="flex-shrink-0">
<img className="h-10 w-10 mr-2" src={ig.imageSrc} alt={ig.title} />
</div>
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
<div>
<p className="text-sm font-medium text-neutral-900 truncate">{ig.title}</p>
<p className="flex items-center text-sm text-gray-500">
{ig.type.endsWith("_calendar") && (
<span className="truncate">Calendar Integration</span>
)}
{ig.type.endsWith("_video") && (
<span className="truncate">Video Conferencing</span>
)}
</p>
</div>
<div className="hidden md:block">
{ig.credential.key && (
<p className="mt-2 flex items-center text text-gray-500">
<CheckCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-green-400" />
Connected
</p>
)}
{!ig.credential.key && (
<p className="mt-3 flex items-center text text-gray-500">
<XCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-yellow-400" />
Not connected
</p>
)}
</div>
</div>
<div>
<ChevronRightIcon className="h-5 w-5 text-gray-400" />
</div>
</div>
</div>
</a>
</Link>
</li>
))}
</ul>
) : (
<div className="bg-white shadow rounded-sm">
<div className="flex">
<div className="py-9 pl-8">
<InformationCircleIcon className="text-neutral-900 w-16" />
</div>
<div className="py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
You don&apos;t have any integrations added.
</h3>
<div className="mt-2 text-sm text-gray-500">
<p>
You currently do not have any integrations set up. Add your first integration to get
started.
</p>
</div>
<div className="mt-3 text-sm">
<button
onClick={toggleAddModal}
className="font-medium text-neutral-900 hover:text-neutral-500">
{" "}
Add your first integration <span aria-hidden="true">&rarr;</span>
</button>
</div>
</div>
</div>
</div>
)}
</div>
{showAddModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<PlusIcon className="h-6 w-6 text-neutral-900" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Add a new integration
</h3>
<div>
<p className="text-sm text-gray-400">Link a new integration to your account.</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{integrations
.filter((integration) => integration.installed)
.map((integration) => (
<li key={integration.type} className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img
className="h-8 w-8 mr-2"
src={integration.imageSrc}
alt={integration.title}
/>
</div>
<div className="w-10/12">
<h2 className="text-gray-800 font-medium">{integration.title}</h2>
<p className="text-gray-400 text-sm">{integration.description}</p>
</div>
<div className="w-2/12 text-right pt-2">
<button
onClick={() => integrationHandler(integration.type)}
className="font-medium text-neutral-900 hover:text-neutral-500">
Add
</button>
</div>
</li>
))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
onClick={toggleAddModal}
type="button"
className="mt-3 w-full inline-flex justify-center rounded-sm border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
)}
<div className="bg-white shadow rounded-sm">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Select calendars</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>Select which calendars are checked for availability to prevent double bookings.</p>
</div>
<div className="mt-5">
<button type="button" onClick={toggleShowCalendarModal} className="btn btn-primary">
Select calendars
</button>
</div>
</div>
</div>
{showSelectCalendarModal && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
{/* <!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
--> */}
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
{/* <!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
--> */}
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<CalendarIcon className="h-6 w-6 text-neutral-900" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
Select calendars
</h3>
<div>
<p className="text-sm text-gray-400">
If no entry is selected, all calendars will be checked
</p>
</div>
</div>
</div>
<div className="my-4">
<ul className="divide-y divide-gray-200">
{selectableCalendars.map((calendar) => (
<li key={calendar.name} className="flex py-4">
<div className="w-1/12 mr-4 pt-2">
<img
className="h-8 w-8 mr-2"
src={getCalendarIntegrationImage(calendar.integration)}
alt={calendar.integration}
/>
</div>
<div className="w-10/12 pt-3">
<h2 className="text-gray-800 font-medium">{calendar.name}</h2>
</div>
<div className="w-2/12 text-right pt-3">
<Switch
checked={calendar.selected}
onChange={calendarSelectionHandler(calendar)}
className={classNames(
calendar.selected ? "bg-neutral-900" : "bg-gray-200",
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500"
)}>
<span className="sr-only">Select calendar</span>
<span
aria-hidden="true"
className={classNames(
calendar.selected ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
</Switch>
</div>
</li>
))}
</ul>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
onClick={toggleShowCalendarModal}
type="button"
className="mt-3 w-full inline-flex justify-center rounded-sm border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500 sm:mt-0 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
)}
</Shell>
</div>
);
}
const validJson = (jsonString: string) => {
try {
const o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
try {
const o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
catch (e) { console.error(e); }
return false;
}
} catch (e) {
console.error(e);
}
return false;
};
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true
}
});
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
},
});
const credentials = await prisma.credential.findMany({
where: {
userId: user.id,
},
select: {
id: true,
type: true,
key: true
}
});
const credentials = await prisma.credential.findMany({
where: {
userId: user.id,
},
select: {
id: true,
type: true,
key: true,
},
});
const integrations = [ {
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
credential: credentials.find( (integration) => integration.type === "google_calendar" ) || null,
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.png",
description: "For personal and business calendars",
}, {
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
type: "office365_calendar",
credential: credentials.find( (integration) => integration.type === "office365_calendar" ) || null,
title: "Office 365 / Outlook.com Calendar",
imageSrc: "integrations/office-365.png",
description: "For personal and business calendars",
}, {
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
type: "zoom_video",
credential: credentials.find( (integration) => integration.type === "zoom_video" ) || null,
title: "Zoom",
imageSrc: "integrations/zoom.png",
description: "Video Conferencing",
} ];
const integrations = [
{
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
credential: credentials.find((integration) => integration.type === "google_calendar") || null,
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.png",
description: "For personal and business calendars",
},
{
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
type: "office365_calendar",
credential: credentials.find((integration) => integration.type === "office365_calendar") || null,
title: "Office 365 / Outlook.com Calendar",
imageSrc: "integrations/office-365.png",
description: "For personal and business calendars",
},
{
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
type: "zoom_video",
credential: credentials.find((integration) => integration.type === "zoom_video") || null,
title: "Zoom",
imageSrc: "integrations/zoom.png",
description: "Video Conferencing",
},
];
return {
props: {integrations},
}
return {
props: { integrations },
};
}

View File

@ -13,11 +13,16 @@ export default function Billing(props) {
return (
<Shell heading="Billing">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">
Manage your billing information and cancel your subscription.
</p>
</div>
<Head>
<title>Billing | Calendso</title>
</Head>
<SettingsShell>
<div className="py-6 px-4 sm:p-6 lg:pb-8 lg:col-span-9">
<div className="py-6 lg:pb-8 lg:col-span-9">
<div className="mb-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">
Change your Subscription
@ -63,4 +68,4 @@ export async function getServerSideProps(context) {
return {
props: {user}, // will be passed to the page component as props
}
}
}

View File

@ -1,105 +1,120 @@
import Head from 'next/head';
import Link from 'next/link';
import { useState } from 'react';
import { useRouter } from 'next/router';
import prisma from '../../lib/prisma';
import Modal from '../../components/Modal';
import Shell from '../../components/Shell';
import SettingsShell from '../../components/Settings';
import Avatar from '../../components/Avatar';
import { signIn, useSession, getSession } from 'next-auth/client';
import TimezoneSelect from 'react-timezone-select';
import Head from "next/head";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/router";
import prisma from "../../lib/prisma";
import Modal from "../../components/Modal";
import Shell from "../../components/Shell";
import SettingsShell from "../../components/Settings";
import Avatar from "../../components/Avatar";
import { signIn, useSession, getSession } from "next-auth/client";
import TimezoneSelect from "react-timezone-select";
export default function Embed(props) {
const [ session, loading ] = useSession();
const router = useRouter();
const [session, loading] = useSession();
const router = useRouter();
if (loading) {
return <div className="loader"></div>;
}
if (loading) {
return <div className="loader"></div>;
}
return(
<Shell heading="Embed">
<Head>
<title>Embed | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<SettingsShell>
<div className="py-6 px-4 sm:p-6 lg:pb-8 lg:col-span-9">
<div className="mb-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Iframe Embed</h2>
<p className="mt-1 text-sm text-gray-500">
The easiest way to embed Calendso on your website.
</p>
</div>
<div className="grid grid-cols-2 space-x-4">
<div>
<label htmlFor="iframe" className="block text-sm font-medium text-gray-700">
Standard iframe
</label>
<div className="mt-1">
<textarea
id="iframe"
className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Loading..."
defaultValue={'<iframe src="' + props.BASE_URL + '/' + session.user.username + '" frameborder="0" allowfullscreen></iframe>'}
readOnly
/>
</div>
</div>
<div>
<label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700">
Responsive full screen iframe
</label>
<div className="mt-1">
<textarea
id="fullscreen"
className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Loading..."
defaultValue={'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Schedule a meeting</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body><iframe src="' + props.BASE_URL + '/' + session.user.username + '" frameborder="0" allowfullscreen></iframe></body></html>'}
readOnly
/>
</div>
</div>
</div>
<div className="my-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Calendso API</h2>
<p className="mt-1 text-sm text-gray-500">
Leverage our API for full control and customizability.
</p>
</div>
<a href="https://api.docs.calendso.com" className="btn btn-primary">Browse our API documentation</a>
</div>
</SettingsShell>
</Shell>
);
return (
<Shell heading="Embed">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">Integrate with your website using our embed options.</p>
</div>
<Head>
<title>Embed | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<SettingsShell>
<div className="py-6 lg:pb-8 lg:col-span-9">
<div className="mb-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Iframe Embed</h2>
<p className="mt-1 text-sm text-gray-500">The easiest way to embed Calendso on your website.</p>
</div>
<div className="grid grid-cols-2 space-x-4">
<div>
<label htmlFor="iframe" className="block text-sm font-medium text-gray-700">
Standard iframe
</label>
<div className="mt-1">
<textarea
id="iframe"
className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Loading..."
defaultValue={
'<iframe src="' +
props.BASE_URL +
"/" +
session.user.username +
'" frameborder="0" allowfullscreen></iframe>'
}
readOnly
/>
</div>
</div>
<div>
<label htmlFor="fullscreen" className="block text-sm font-medium text-gray-700">
Responsive full screen iframe
</label>
<div className="mt-1">
<textarea
id="fullscreen"
className="h-32 shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Loading..."
defaultValue={
'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Schedule a meeting</title><style>body {margin: 0;}iframe {height: calc(100vh - 4px);width: calc(100vw - 4px);box-sizing: border-box;}</style></head><body><iframe src="' +
props.BASE_URL +
"/" +
session.user.username +
'" frameborder="0" allowfullscreen></iframe></body></html>'
}
readOnly
/>
</div>
</div>
</div>
<div className="my-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Calendso API</h2>
<p className="mt-1 text-sm text-gray-500">
Leverage our API for full control and customizability.
</p>
</div>
<a href="https://api.docs.calendso.com" className="btn btn-primary">
Browse our API documentation
</a>
</div>
</SettingsShell>
</Shell>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
}
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
timeZone: true,
weekStart: true,
}
});
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
timeZone: true,
weekStart: true,
},
});
const BASE_URL = process.env.BASE_URL;
const BASE_URL = process.env.BASE_URL;
return {
props: {user, BASE_URL}, // will be passed to the page component as props
}
return {
props: { user, BASE_URL }, // will be passed to the page component as props
};
}

View File

@ -1,105 +1,130 @@
import Head from 'next/head';
import Link from 'next/link';
import { useRef, useState } from 'react';
import prisma from '../../lib/prisma';
import Modal from '../../components/Modal';
import Shell from '../../components/Shell';
import SettingsShell from '../../components/Settings';
import { signIn, useSession, getSession } from 'next-auth/client';
import Head from "next/head";
import Link from "next/link";
import { useRef, useState } from "react";
import prisma from "../../lib/prisma";
import Modal from "../../components/Modal";
import Shell from "../../components/Shell";
import SettingsShell from "../../components/Settings";
import { signIn, useSession, getSession } from "next-auth/client";
export default function Settings(props) {
const [ session, loading ] = useSession();
const [successModalOpen, setSuccessModalOpen] = useState(false);
const oldPasswordRef = useRef<HTMLInputElement>();
const newPasswordRef = useRef<HTMLInputElement>();
const [session, loading] = useSession();
const [successModalOpen, setSuccessModalOpen] = useState(false);
const oldPasswordRef = useRef<HTMLInputElement>();
const newPasswordRef = useRef<HTMLInputElement>();
if (loading) {
return <div className="loader"></div>;
}
if (loading) {
return <div className="loader"></div>;
}
const closeSuccessModal = () => { setSuccessModalOpen(false); }
const closeSuccessModal = () => {
setSuccessModalOpen(false);
};
async function changePasswordHandler(event) {
event.preventDefault();
async function changePasswordHandler(event) {
event.preventDefault();
const enteredOldPassword = oldPasswordRef.current.value;
const enteredNewPassword = newPasswordRef.current.value;
const enteredOldPassword = oldPasswordRef.current.value;
const enteredNewPassword = newPasswordRef.current.value;
// TODO: Add validation
// TODO: Add validation
const response = await fetch('/api/auth/changepw', {
method: 'PATCH',
body: JSON.stringify({oldPassword: enteredOldPassword, newPassword: enteredNewPassword}),
headers: {
'Content-Type': 'application/json'
}
});
const response = await fetch("/api/auth/changepw", {
method: "PATCH",
body: JSON.stringify({ oldPassword: enteredOldPassword, newPassword: enteredNewPassword }),
headers: {
"Content-Type": "application/json",
},
});
setSuccessModalOpen(true);
}
setSuccessModalOpen(true);
}
return(
<Shell heading="Password">
<Head>
<title>Change Password | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<SettingsShell>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg leading-6 font-medium text-gray-900">Change Password</h2>
<p className="mt-1 text-sm text-gray-500">
Change the password for your Calendso account.
</p>
</div>
<div className="mt-6 flex">
<div className="w-1/2 mr-2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">Current Password</label>
<div className="mt-1">
<input ref={oldPasswordRef} type="password" name="current_password" id="current_password" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Your old password" />
</div>
</div>
<div className="w-1/2 ml-2">
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">New Password</label>
<div className="mt-1">
<input ref={newPasswordRef} type="password" name="new_password" id="new_password" required className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="Your super secure new password" />
</div>
</div>
</div>
<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="Password updated successfully" description="Your password has been successfully changed." open={successModalOpen} handleClose={closeSuccessModal} />
</SettingsShell>
</Shell>
);
return (
<Shell heading="Password">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">Change the password that you use to sign in.</p>
</div>
<Head>
<title>Change Password | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<SettingsShell>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-8">
<div className="flex">
<div className="w-1/2 mr-2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
Current Password
</label>
<div className="mt-1">
<input
ref={oldPasswordRef}
type="password"
name="current_password"
id="current_password"
required
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Your old password"
/>
</div>
</div>
<div className="w-1/2 ml-2">
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
New Password
</label>
<div className="mt-1">
<input
ref={newPasswordRef}
type="password"
name="new_password"
id="new_password"
required
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Your super secure new password"
/>
</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-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Save
</button>
</div>
</div>
</form>
<Modal
heading="Password updated successfully"
description="Your password has been successfully changed."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
</SettingsShell>
</Shell>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: '/auth/login' } };
}
const session = await getSession(context);
if (!session) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
name: true
}
});
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
username: true,
name: true,
},
});
return {
props: {user}, // will be passed to the page component as props
}
}
return {
props: { user }, // will be passed to the page component as props
};
}

View File

@ -91,6 +91,11 @@ export default function Settings(props) {
return (
<Shell heading="Profile">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">
Edit your profile information, which shows on your scheduling link.
</p>
</div>
<Head>
<title>Profile | Calendso</title>
<link rel="icon" href="/favicon.ico" />
@ -98,13 +103,8 @@ export default function Settings(props) {
<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>
<div className="mt-6 flex flex-col lg:flex-row">
<div className="py-6 lg:pb-8">
<div className="flex flex-col lg:flex-row">
<div className="flex-grow space-y-6">
<div className="flex">
<div className="w-1/2 mr-2">
@ -122,7 +122,7 @@ export default function Settings(props) {
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"
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>
@ -140,7 +140,7 @@ export default function Settings(props) {
placeholder="A little something about yourself."
rows={3}
defaultValue={props.user.bio}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"></textarea>
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>
@ -152,7 +152,7 @@ export default function Settings(props) {
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"
className="shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-sm"
/>
</div>
</div>
@ -165,7 +165,7 @@ export default function Settings(props) {
id="weekStart"
value={selectedWeekStartDay}
onChange={setSelectedWeekStartDay}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
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={[
{ value: "Sunday", label: "Sunday" },
{ value: "Monday", label: "Monday" },
@ -183,11 +183,11 @@ export default function Settings(props) {
isDisabled={!selectedTheme}
defaultValue={selectedTheme || themeOptions[0]}
onChange={setSelectedTheme}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
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="relative flex items-start">
<div className="mt-8 relative flex items-start">
<div className="flex items-center h-5">
<input
id="theme-adjust-os"
@ -195,7 +195,7 @@ export default function Settings(props) {
type="checkbox"
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
defaultChecked={!selectedTheme}
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm"
/>
</div>
<div className="ml-3 text-sm">
@ -214,7 +214,7 @@ export default function Settings(props) {
type="checkbox"
ref={hideBrandingRef}
defaultChecked={props.user.hideBranding}
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm"
/>
</div>
<div className="ml-3 text-sm">
@ -238,13 +238,13 @@ export default function Settings(props) {
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="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-md" />
<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>
@ -254,12 +254,12 @@ export default function Settings(props) {
<Avatar
user={props.user}
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-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-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-sm" />
</label> */}
</div>
<div className="mt-4">
@ -272,7 +272,7 @@ export default function Settings(props) {
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"
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>
@ -282,7 +282,7 @@ export default function Settings(props) {
<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">
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>

View File

@ -58,21 +58,22 @@ export default function Teams() {
return (
<Shell heading="Teams">
<div className="flex mb-8">
<p className="text-sm text-neutral-500">
Create and manage teams to use collaborative features.
</p>
</div>
<Head>
<title>Teams | Calendso</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<SettingsShell>
<div className="divide-y divide-gray-200 lg:col-span-9">
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="py-6 lg:pb-8">
<div className="flex justify-between">
<div>
<h2 className="text-lg leading-6 font-medium text-gray-900">Your teams</h2>
<p className="mt-1 text-sm text-gray-500 mb-4">
View, edit and create teams to organise relationships between users
</p>
{!(invites.length || teams.length) && (
<div className="bg-gray-50 sm:rounded-lg">
<div className="bg-gray-50 sm:rounded-sm">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Create a team to get started
@ -94,7 +95,7 @@ export default function Teams() {
</div>
{!!(invites.length || teams.length) && (
<div>
<button className="btn-sm btn-primary" onClick={() => setShowCreateTeamModal(true)}>
<button className="btn-sm btn-primary mb-4" onClick={() => setShowCreateTeamModal(true)}>
Create new team
</button>
</div>
@ -143,10 +144,10 @@ export default function Teams() {
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<UsersIcon className="h-6 w-6 text-blue-600" />
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<UsersIcon className="h-6 w-6 text-neutral-900" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
@ -168,7 +169,7 @@ export default function Teams() {
id="name"
placeholder="Acme Inc."
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"
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"
/>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">

View File

@ -1,41 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 427 97.5" style="enable-background:new 0 0 427 97.5;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#104D86;}
</style>
<path class="st0" d="M27.5,88.2c-4.9,0-9.7-1.2-14-3.6c-4.2-2.4-7.6-5.8-9.9-10c-4.8-8.8-4.8-19.4,0-28.2c2.3-4.2,5.8-7.7,10-10
c4.3-2.4,9.1-3.7,14-3.6c6-0.1,11.8,1.7,16.5,5.3s8,8.7,9.9,15.4H42.8c-1.3-3-3.4-5.5-6.2-7.2c-2.6-1.6-5.6-2.5-8.7-2.5
c-2.8,0-5.6,0.7-8.1,2s-4.7,3.3-6.1,5.8c-3,5.3-3.1,11.9-0.2,17.3c1.4,2.5,3.4,4.5,5.8,6s5.2,2.3,8,2.2c7.2,0,12.4-3.3,15.4-9.9
h11.4c-1.1,4.2-3.1,8.1-5.8,11.5c-2.5,3-5.6,5.4-9.2,7C35.5,87.4,31.5,88.1,27.5,88.2L27.5,88.2z M99.6,82.1
c-4.6,4-10.6,6.2-16.8,6.1c-4.9,0-9.7-1.2-14-3.6c-4.1-2.4-7.5-5.8-9.8-10c-2.5-4.5-3.7-9.5-3.6-14.6c-0.1-15,11.9-27.2,26.9-27.3
c0.1,0,0.2,0,0.3,0c6.4-0.2,12.6,2.1,17.5,6.2v-5.2h11.5V87h-12V82.1z M83.5,43.7c-3,0-5.9,0.7-8.4,2.2c-2.5,1.4-4.6,3.5-6,6
c-1.5,2.5-2.2,5.4-2.2,8.4c-0.2,4.5,1.5,8.9,4.7,12.2c3.2,3.2,7.6,4.9,12.1,4.8c2.9,0,5.8-0.7,8.3-2.2c2.5-1.4,4.5-3.5,5.9-6
c2.9-5.3,2.9-11.7,0-17c-1.4-2.5-3.5-4.6-6-6C89.3,44.5,86.4,43.8,83.5,43.7L83.5,43.7z M122,14.8h9.7V87H122
C122,87.1,122,14.8,122,14.8z M149.8,65.2c0.5,2.3,1.5,4.4,3,6.3c1.4,1.8,3.3,3.2,5.4,4.1c2.2,1,4.6,1.5,7,1.5
c2.8,0.1,5.6-0.5,8.1-1.8c2.4-1.4,4.3-3.4,5.5-5.9h11.9c-2.7,6.5-6.2,11.2-10.4,14.2c-4.4,3.1-9.8,4.7-15.2,4.5
c-4.8,0.1-9.6-1.2-13.8-3.6c-4.1-2.4-7.5-5.8-9.7-10c-2.4-4.3-3.6-9.2-3.5-14.1c-0.1-4.9,1.1-9.8,3.5-14.1c2.3-4.2,5.7-7.6,9.8-10
c8.6-4.8,19.2-4.8,27.7,0.1c4.1,2.4,7.4,6,9.6,10.2c2.2,4.3,3.3,9.1,3.3,14c0,1-0.1,2.5-0.3,4.5h-41.9V65.2z M165.2,43.6
c-3.4-0.1-6.8,0.9-9.6,2.9c-2.7,2-4.7,4.8-5.6,8h30.2c-0.9-3.2-2.9-6-5.6-8C171.9,44.5,168.6,43.5,165.2,43.6z M234.8,57.3
c0-4.7-1-8.2-3-10.5s-4.9-3.5-8.7-3.5c-2.3,0-4.6,0.7-6.5,2c-2,1.3-3.7,3.2-4.8,5.4c-1.2,2.3-1.8,4.8-1.8,7.4V87h-11.2V33.8h10.5
v4.8c3.9-3.9,9.3-6,14.9-5.8c3.9,0,7.7,1,11,3s6,4.8,7.9,8.2c1.9,3.5,2.9,7.5,2.9,11.5v31.6h-11.2L234.8,57.3L234.8,57.3z
M296.7,82.2c-2.4,1.9-5,3.4-7.9,4.4c-3,1-6.2,1.5-9.4,1.5c-4.8,0.1-9.6-1.2-13.8-3.7c-4.1-2.4-7.5-5.9-9.8-10.1
c-2.4-4.3-3.6-9.1-3.6-14.1c-0.1-5.1,1.1-10.1,3.5-14.7c2.3-4.2,5.7-7.7,9.8-10.1c4.3-2.5,9.2-3.8,14.1-3.7c3.1,0,6.1,0.5,9,1.4
c2.8,0.9,5.4,2.3,7.8,4.1V14.9h11.3v72.2h-11.1L296.7,82.2L296.7,82.2z M280.4,43.1c-3,0-5.9,0.7-8.4,2.2c-2.5,1.4-4.6,3.5-6,6
c-1.5,2.6-2.2,5.5-2.2,8.5c-0.2,4.6,1.5,9.1,4.7,12.4c3.2,3.2,7.5,5,12.1,4.8c2.9,0,5.8-0.7,8.3-2.3c2.5-1.5,4.5-3.6,5.9-6.1
c2.9-5.4,2.9-11.8,0-17.2C291.8,46.2,286.3,43,280.4,43.1z M335.8,88.2c-3.8,0.1-7.5-0.8-10.8-2.4c-3.1-1.6-5.7-4-7.5-7
c-1.9-3.3-2.9-7.1-3-10.9h11c0,2.7,1.1,5.3,3.1,7.1c2.1,1.7,4.8,2.6,7.5,2.5c1.8,0,3.6-0.2,5.3-0.8c1.3-0.4,2.4-1.2,3.3-2.2
c0.7-0.8,1.1-1.9,1.1-3c0.1-1.2-0.4-2.4-1.2-3.2c-0.5-0.5-1.1-0.8-1.7-1.2c-0.7-0.3-1.4-0.6-2.2-0.8c-1.8-0.4-3.9-0.9-6.1-1.3
c-1.8-0.2-3.5-0.6-5.1-1s-3.5-1-5-1.6c-0.8-0.2-1.6-0.6-2.4-1l-1-0.7c-0.4-0.4-0.8-0.7-1.2-1c-0.7-0.5-1.3-1.1-1.8-1.8
c-0.6-0.8-1.1-1.7-1.5-2.6c-0.9-2-1.3-4.2-1.2-6.4c0-3,0.9-6,2.7-8.5c1.8-2.5,4.3-4.5,7.2-5.7c3.3-1.4,6.8-2.1,10.3-2
c3.6-0.1,7.1,0.7,10.3,2.4c3,1.5,5.4,3.8,7.1,6.7c1.8,3.2,2.7,6.8,2.7,10.4H345c-0.1-3-1-5.2-2.7-6.7s-4-2.3-7-2.3s-5.2,0.5-6.8,1.5
c-1.4,0.8-2.3,2.2-2.3,3.8c-0.1,1,0.3,2,1,2.6c0.6,0.5,1.2,0.9,1.8,1.2c0.6,0.3,1.3,0.5,2,0.7c0.8,0.2,1.7,0.4,2.7,0.6l3.1,0.5
c3.4,0.5,6.7,1.2,10,2.2c2.6,0.8,5,2.3,6.7,4.5c1.8,2.4,2.9,5.3,3,8.4l0.1,1.8c0.1,3.2-0.9,6.4-2.8,9.1s-4.5,4.8-7.5,6
C343.2,87.6,339.5,88.3,335.8,88.2L335.8,88.2z M412.4,74.6c-2.4,4.2-5.9,7.7-10.2,10c-4.5,2.4-9.6,3.7-14.8,3.6
c-4.9,0.1-9.7-1.2-14-3.6c-4.2-2.4-7.7-5.8-10.1-9.9c-2.5-4.2-3.8-9-3.7-13.8c-0.1-5.1,1.1-10.1,3.6-14.6c2.4-4.2,6-7.7,10.2-10
c4.4-2.4,9.3-3.6,14.3-3.6s9.9,1.2,14.3,3.6c4.2,2.3,7.8,5.7,10.2,9.9c2.4,4.3,3.7,9.2,3.6,14.2C416.1,65.4,414.8,70.3,412.4,74.6
L412.4,74.6z M387.8,43.3c-3,0-6,0.8-8.6,2.3s-4.8,3.6-6.3,6.2c-1.5,2.6-2.3,5.5-2.3,8.5c0,4.6,1.8,9,5.2,12.2
c3.2,3.4,7.6,5.3,12.3,5.3c3,0,6-0.8,8.5-2.4c2.6-1.5,4.7-3.7,6.2-6.3c1.5-2.6,2.3-5.6,2.3-8.6s-0.8-6-2.3-8.6s-3.7-4.7-6.3-6.2
C393.9,44.1,390.9,43.3,387.8,43.3z"/>
<svg width="104" height="20" viewBox="0 0 104 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1C0 0.447715 0.447715 0 1 0H19C19.5523 0 20 0.447715 20 1V19C20 19.5523 19.5523 20 19 20H10.4142C10.149 20 9.89464 19.8946 9.70711 19.7071L5.21739 15.2174L0.292893 10.2929C0.105357 10.1054 0 9.851 0 9.58579V1Z" fill="black"/>
<path d="M32.0349 15.426C34.2609 15.426 35.8783 14.226 36.1914 12.313L33.5827 11.7391C33.4262 12.7826 32.887 13.3565 32.0175 13.3565C30.9218 13.3565 30.2957 12.4173 30.2957 10.7826C30.2957 9.26951 30.9392 8.36516 32.0001 8.36516C32.8522 8.36516 33.4088 8.90429 33.5653 9.93038L36.1914 9.39125C35.8609 7.42603 34.3479 6.31299 32.0696 6.31299C29.2175 6.31299 27.4609 8.03473 27.4609 10.8695C27.4609 13.6347 29.287 15.426 32.0349 15.426Z" fill="black"/>
<path d="M40.0978 15.426C41.4543 15.426 42.5673 14.8173 43.0021 13.8434C43.0195 14.3304 43.0543 14.7999 43.1412 15.2173H45.8543C45.6978 14.6782 45.6108 13.826 45.6108 12.7999V9.91299C45.6108 7.40864 44.3586 6.31299 41.5586 6.31299C39.0891 6.31299 37.5238 7.46082 37.3847 9.39125H40.1499C40.2021 8.55647 40.6891 8.10429 41.5412 8.10429C42.4108 8.10429 42.8282 8.53908 42.8282 9.53038V9.82603L40.7238 10.1217C39.4021 10.2956 38.5673 10.5739 37.9934 10.9739C37.4021 11.3912 37.1065 12.0347 37.1065 12.8521C37.1065 14.3999 38.3065 15.426 40.0978 15.426ZM41.1412 13.6521C40.4108 13.6521 39.9238 13.2521 39.9238 12.6434C39.9238 11.9999 40.3934 11.6347 41.4195 11.4434L42.9151 11.1999V11.8956C42.9151 12.9391 42.2021 13.6521 41.1412 13.6521Z" fill="black"/>
<path d="M50.6759 2.60864H47.8759V15.2173H50.6759V2.60864Z" fill="black"/>
<path d="M61.1052 10.6608C61.1052 7.8956 59.5748 6.31299 56.8444 6.31299C54.1313 6.31299 52.427 8.10429 52.427 10.9565C52.427 13.7739 54.1661 15.426 57.0357 15.426C58.8444 15.426 60.3226 14.5739 61.0183 13.1478L58.9661 12.2086C58.5661 13.0086 57.9226 13.426 57.0704 13.426C55.9052 13.426 55.0878 12.6782 55.0704 11.4956H61.1052V10.6608ZM58.4096 9.93038H55.0704C55.1052 8.6956 55.7139 7.96516 56.7922 7.96516C57.8357 7.96516 58.4096 8.62603 58.4096 9.86082V9.93038Z" fill="black"/>
<path d="M62.8535 15.2173H65.6535V10.5217C65.6535 9.18256 66.1926 8.43473 67.1665 8.43473C68.0535 8.43473 68.4709 9.00864 68.4709 10.2086V15.2173H71.2535V9.82603C71.2535 7.4956 70.2969 6.2956 68.4187 6.2956C67.0969 6.2956 66.1404 6.92169 65.5317 8.19125V6.52169H62.8535V15.2173Z" fill="black"/>
<path d="M76.3209 15.426C77.5904 15.426 78.6339 14.7652 79.0687 13.7217V15.2173H81.7122V2.60864H78.9295V7.6869C78.4948 6.78256 77.6252 6.31299 76.4774 6.31299C74.3035 6.31299 72.8426 8.13908 72.8426 10.9043C72.8426 13.6173 74.2339 15.426 76.3209 15.426ZM77.4165 13.4956C76.3556 13.4956 75.7122 12.4869 75.7122 10.8521C75.7122 9.18256 76.3209 8.24342 77.3817 8.24342C78.373 8.24342 78.9643 9.06082 78.9643 10.5391V11.2869C78.9643 12.6434 78.373 13.4956 77.4165 13.4956Z" fill="black"/>
<path d="M87.5423 15.426C89.9771 15.426 91.3858 14.3304 91.3858 12.5565C91.3858 11.0608 90.6554 10.1391 88.7945 9.87821L87.2467 9.63473C86.4815 9.53038 86.2206 9.30429 86.2206 8.8869C86.2206 8.39995 86.6032 8.17386 87.438 8.17386C88.5162 8.17386 89.3858 8.60864 90.0293 9.33908L91.4728 8.03473C90.7597 6.95647 89.2815 6.31299 87.5249 6.31299C85.1249 6.31299 83.6988 7.37386 83.6988 9.06082C83.6988 10.5391 84.5858 11.4608 86.4641 11.7391L87.7162 11.913C88.6728 12.0521 88.951 12.2434 88.951 12.7652C88.951 13.2521 88.4988 13.5304 87.5945 13.5304C86.3945 13.5304 85.4554 13.0608 84.8467 12.1217L83.2293 13.3391C84.0641 14.713 85.6293 15.426 87.5423 15.426Z" fill="black"/>
<path d="M96.9109 15.426C99.4848 15.426 101.433 13.8434 101.433 10.8521C101.433 7.86082 99.4848 6.31299 96.9109 6.31299C94.3544 6.31299 92.4066 7.86082 92.4066 10.8521C92.4066 13.8434 94.3544 15.426 96.9109 15.426ZM96.9109 13.4956C95.8327 13.4956 95.1892 12.626 95.1892 10.8521C95.1892 9.06082 95.8327 8.24342 96.9109 8.24342C98.0066 8.24342 98.6501 9.06082 98.6501 10.8521C98.6501 12.626 98.0066 13.4956 96.9109 13.4956Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -7,84 +7,84 @@
@layer components {
/* Primary buttons */
.btn-xs.btn-primary {
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-sm.btn-primary {
@apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn.btn-primary {
@apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-lg.btn-primary {
@apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-xl.btn-primary {
@apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-wide.btn-primary {
@apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm shadow-sm text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
/* Secondary buttons */
.btn-xs.btn-secondary {
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-sm.btn-secondary {
@apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn.btn-secondary {
@apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-lg.btn-secondary {
@apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-xl.btn-secondary {
@apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-wide.btn-secondary {
@apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply w-full text-center px-4 py-2 border border-transparent text-sm font-medium rounded-sm text-neutral-700 bg-neutral-100 hover:bg-neutral-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
/* White buttons */
.btn-xs.btn-white {
@apply inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-sm.btn-white {
@apply inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn.btn-white {
@apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-lg.btn-white {
@apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-xl.btn-white {
@apply inline-flex items-center px-6 py-3 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply inline-flex items-center px-6 py-3 border border-gray-300 shadow-sm text-base font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
.btn-wide.btn-white {
@apply w-full text-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
@apply w-full text-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;
}
}
.loader {
margin: 80px auto;
border: 8px solid #f3f3f3; /* Light grey */
border-top: 8px solid #039be5; /* Blue */
border-top: 8px solid #039be5; /* neutral */
border-radius: 50%;
width: 60px;
height: 60px;
@ -105,11 +105,11 @@ nav#nav--settings > a svg {
}
nav#nav--settings > a.active {
@apply bg-blue-50 border-blue-500 text-blue-700 hover:bg-blue-50 hover:text-blue-700;
@apply bg-neutral-50 border-neutral-500 text-neutral-700 hover:bg-neutral-50 hover:text-neutral-700;
}
nav#nav--settings > a.active svg {
@apply text-blue-500;
@apply text-neutral-500;
}
@ -155,4 +155,4 @@ body {
rgba(3, 169, 244, var(--tw-border-opacity))
rgba(3, 169, 244, var(--tw-border-opacity))
white;
}
}

View File

@ -5,27 +5,77 @@ module.exports = {
theme: {
extend: {
colors: {
gray: {
100: "#EBF1F5",
200: "#D9E3EA",
300: "#C5D2DC",
400: "#9BA9B4",
500: "#707D86",
600: "#55595F",
700: "#33363A",
800: "#25282C",
900: "#151719",
neutral: {
50: "#F7F8F9",
100: "#F4F5F6",
200: "#EAEEF2",
300: "#C6CCD5",
400: "#9BA6B6",
500: "#708097",
600: "#657388",
700: "#373F4A",
800: "#1F2937",
900: "#1A1A1A",
},
blue: {
100: "#b3e5fc",
200: "#81d4fa",
300: "#4fc3f7",
400: "#29b6f6",
500: "#03a9f4",
600: "#039be5",
700: "#0288d1",
800: "#0277bd",
900: "#01579b",
primary: {
50: "#F4F4F4",
100: "#E8E8E8",
200: "#C6C6C6",
300: "#A3A3A3",
400: "#5F5F5F",
500: "#1A1A1A",
600: "#171717",
700: "#141414",
800: "#101010",
900: "#0D0D0D",
},
secondary: {
50: "#F5F8F7",
100: "#EBF0F0",
200: "#CDDAD9",
300: "#AEC4C2",
400: "#729894",
500: "#356C66",
600: "#30615C",
700: "#28514D",
800: "#20413D",
900: "#223B41",
},
red: {
50: "#FEF2F2",
100: "#FEE2E2",
200: "#FECACA",
300: "#FCA5A5",
400: "#F87171",
500: "#EF4444",
600: "#DC2626",
700: "#B91C1C",
800: "#991B1B",
900: "#7F1D1D",
},
orange: {
50: "#FFF7ED",
100: "#FFEDD5",
200: "#FED7AA",
300: "#FDBA74",
400: "#FB923C",
500: "#F97316",
600: "#EA580C",
700: "#C2410C",
800: "#9A3412",
900: "#7C2D12",
},
green: {
50: "#ECFDF5",
100: "#D1FAE5",
200: "#A7F3D0",
300: "#6EE7B7",
400: "#34D399",
500: "#10B981",
600: "#059669",
700: "#047857",
800: "#065F46",
900: "#064E3B",
},
},
maxHeight: (theme) => ({