import {useEffect, useState, useMemo} from 'react'; import Head from 'next/head'; import Link from 'next/link'; import prisma from '../../lib/prisma'; import { useRouter } from 'next/router'; import dayjs, { Dayjs } from 'dayjs'; import { Switch } from '@headlessui/react'; import TimezoneSelect from 'react-timezone-select'; import { ClockIcon, GlobeIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'; import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; import isBetween from 'dayjs/plugin/isBetween'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(isSameOrBefore); dayjs.extend(isBetween); dayjs.extend(utc); dayjs.extend(timezone); import getSlots from '../../lib/slots'; import {useTelemetry} from "../../lib/telemetry"; function classNames(...classes) { return classes.filter(Boolean).join(' ') } export default function Type(props) { // Initialise state const [selectedDate, setSelectedDate] = useState<Dayjs>(); const [selectedMonth, setSelectedMonth] = useState(dayjs().month()); const [loading, setLoading] = useState(false); const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); const [is24h, setIs24h] = useState(false); const [busy, setBusy] = useState([]); const telemetry = useTelemetry(); const [selectedTimeZone, setSelectedTimeZone] = useState(''); function toggleTimeOptions() { setIsTimeOptionsOpen(!isTimeOptionsOpen); } useEffect(() => { // Setting timezone only client-side setSelectedTimeZone(dayjs.tz.guess()) }, []) // Get router variables const router = useRouter(); const { user } = router.query; // Handle month changes const incrementMonth = () => { setSelectedMonth(selectedMonth + 1); } const decrementMonth = () => { setSelectedMonth(selectedMonth - 1); } // Need to define the bounds of the 24-hour window const lowerBound = useMemo(() => { if(!selectedDate) { return } return selectedDate.startOf('day') }, [selectedDate]) const upperBound = useMemo(() => { if(!selectedDate) return return selectedDate.endOf('day') }, [selectedDate]) // Set up calendar var daysInMonth = dayjs().month(selectedMonth).daysInMonth(); var days = []; for (let i = 1; i <= daysInMonth; i++) { days.push(i); } // Create placeholder elements for empty days in first week const weekdayOfFirst = dayjs().month(selectedMonth).date(1).day(); const emptyDays = Array(weekdayOfFirst).fill(null).map((day, i) => <div key={`e-${i}`} className={"text-center w-10 h-10 rounded-full mx-auto"}> {null} </div> ); // Combine placeholder days with actual days const calendar = [...emptyDays, ...days.map((day) => <button key={day} onClick={(e) => { telemetry.withJitsu((jitsu) => jitsu.track('date_selected', {page_title: "", source_ip: ""})) setSelectedDate(dayjs().tz(selectedTimeZone).month(selectedMonth).date(day)) }} disabled={selectedMonth < parseInt(dayjs().format('MM')) && dayjs().month(selectedMonth).format("D") > day} className={"text-center w-10 h-10 rounded-full mx-auto " + (dayjs().isSameOrBefore(dayjs().date(day).month(selectedMonth)) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-400 font-light') + (dayjs(selectedDate).month(selectedMonth).format("D") == day ? ' bg-blue-600 text-white-important' : '')}> {day} </button> )]; // Handle date change and timezone change useEffect(() => { const changeDate = async () => { if (!selectedDate) { return } setLoading(true); const res = await fetch(`/api/availability/${user}?dateFrom=${lowerBound.utc().format()}&dateTo=${upperBound.utc().format()}`); const busyTimes = await res.json(); if (busyTimes.length > 0) setBusy(busyTimes); setLoading(false); } changeDate(); }, [selectedDate, selectedTimeZone]); const times = useMemo(() => getSlots({ calendarTimeZone: props.user.timeZone, selectedTimeZone: selectedTimeZone, eventLength: props.eventType.length, selectedDate: selectedDate, dayStartTime: props.user.startTime, dayEndTime: props.user.endTime, }) , [selectedDate, selectedTimeZone]) // Check for conflicts for(let i = times.length - 1; i >= 0; i -= 1) { busy.forEach(busyTime => { let startTime = dayjs(busyTime.start); let endTime = dayjs(busyTime.end); // Check if start times are the same if (dayjs(times[i]).format('HH:mm') == startTime.format('HH:mm')) { times.splice(i, 1); } // Check if time is between start and end times if (dayjs(times[i]).isBetween(startTime, endTime)) { times.splice(i, 1); } }); } // Display available times const availableTimes = times.map((time) => <div key={dayjs(time).utc().format()}> <Link href={`/${props.user.username}/book?date=${dayjs(time).utc().format()}&type=${props.eventType.id}`}> <a key={dayjs(time).format("hh:mma")} className="block font-medium mb-4 text-blue-600 border border-blue-600 rounded hover:text-white hover:bg-blue-600 py-4">{dayjs(time).tz(selectedTimeZone).format(is24h ? "HH:mm" : "hh:mma")}</a> </Link> </div> ); return ( <div> <Head> <title> {props.eventType.title} | {props.user.name || props.user.username} | Calendso </title> <link rel="icon" href="/favicon.ico" /> </Head> <main className={ "mx-auto my-24 transition-max-width ease-in-out duration-500 " + (selectedDate ? "max-w-6xl" : "max-w-3xl") } > <div className="bg-white shadow rounded-lg"> <div className="sm:flex px-4 py-5 sm:p-4"> <div className={ "pr-8 sm:border-r " + (selectedDate ? "sm:w-1/3" : "sm:w-1/2") } > {props.user.avatar && ( <img src={props.user.avatar} alt="Avatar" className="w-16 h-16 rounded-full mb-4" /> )} <h2 className="font-medium text-gray-500">{props.user.name}</h2> <h1 className="text-3xl font-semibold text-gray-800 mb-4"> {props.eventType.title} </h1> <p className="text-gray-500 mb-1 px-2 py-1 -ml-2"> <ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> {props.eventType.length} minutes </p> <button onClick={toggleTimeOptions} className="text-gray-500 mb-1 px-2 py-1 -ml-2" > <GlobeIcon className="inline-block w-4 h-4 mr-1 -mt-1" /> {selectedTimeZone} <ChevronDownIcon className="inline-block w-4 h-4 ml-1 -mt-1" /> </button> {isTimeOptionsOpen && ( <div className="w-full rounded shadow border bg-white px-4 py-2"> <div className="flex mb-4"> <div className="w-1/2 font-medium">Time Options</div> <div className="w-1/2"> <Switch.Group as="div" className="flex items-center justify-end" > <Switch.Label as="span" className="mr-3"> <span className="text-sm text-gray-500">am/pm</span> </Switch.Label> <Switch checked={is24h} onChange={setIs24h} className={classNames( is24h ? "bg-blue-600" : "bg-gray-200", "relative inline-flex flex-shrink-0 h-5 w-8 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-blue-500" )} > <span className="sr-only">Use setting</span> <span aria-hidden="true" className={classNames( is24h ? "translate-x-3" : "translate-x-0", "pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200" )} /> </Switch> <Switch.Label as="span" className="ml-3"> <span className="text-sm text-gray-500">24h</span> </Switch.Label> </Switch.Group> </div> </div> <TimezoneSelect id="timeZone" value={selectedTimeZone} onChange={({ value }) => setSelectedTimeZone(value)} className="mb-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" /> </div> )} <p className="text-gray-600 mt-3 mb-8"> {props.eventType.description} </p> </div> <div className={ "mt-8 sm:mt-0 " + (selectedDate ? "sm:w-1/3 border-r sm:px-4" : "sm:w-1/2 sm:pl-4") } > <div className="flex text-gray-600 font-light text-xl mb-4 ml-2"> <span className="w-1/2"> {dayjs().month(selectedMonth).format("MMMM YYYY")} </span> <div className="w-1/2 text-right"> <button onClick={decrementMonth} className={ "mr-4 " + (selectedMonth < parseInt(dayjs().format("MM")) && "text-gray-400") } disabled={selectedMonth < parseInt(dayjs().format("MM"))} > <ChevronLeftIcon className="w-5 h-5" /> </button> <button onClick={incrementMonth}> <ChevronRightIcon className="w-5 h-5" /> </button> </div> </div> <div className="grid grid-cols-7 gap-y-4 text-center"> <div className="uppercase text-gray-400 text-xs tracking-widest"> Sun </div> <div className="uppercase text-gray-400 text-xs tracking-widest"> Mon </div> <div className="uppercase text-gray-400 text-xs tracking-widest"> Tue </div> <div className="uppercase text-gray-400 text-xs tracking-widest"> Wed </div> <div className="uppercase text-gray-400 text-xs tracking-widest"> Thu </div> <div className="uppercase text-gray-400 text-xs tracking-widest"> Fri </div> <div className="uppercase text-gray-400 text-xs tracking-widest"> Sat </div> {calendar} </div> </div> {selectedDate && ( <div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:max-h-97 overflow-y-auto"> <div className="text-gray-600 font-light text-xl mb-4 text-left"> <span className="w-1/2"> {dayjs(selectedDate).format("dddd DD MMMM YYYY")} </span> </div> {!loading ? availableTimes : <div className="loader"></div>} </div> )} </div> </div> {/* note(peer): you can remove calendso branding here, but we'd also appreciate it, if you don't <3 */} <div className="text-xs text-right pt-1"> <Link href="/pricing"> <a className="text-gray-800 opacity-50 hover:opacity-100"> powered by{" "} <img style={{ top: -2 }} className="w-auto inline h-3 relative" src="calendso-logo-white-word.svg" alt="Calendso Logo" /> </a> </Link> </div> </main> </div> ); } export async function getServerSideProps(context) { const user = await prisma.user.findFirst({ where: { username: context.query.user, }, select: { id: true, username: true, name: true, bio: true, avatar: true, eventTypes: true, startTime: true, timeZone: true, endTime: true } }); const eventType = await prisma.eventType.findFirst({ where: { userId: user.id, slug: { equals: context.query.type, }, }, select: { id: true, title: true, description: true, length: true } }); return { props: { user, eventType }, } }