cal.pub0.org/apps/web/pages/old-getting-started.tsx

743 lines
24 KiB
TypeScript
Raw Normal View History

Feature/new onboarding page (#3377) * [WIP] New design and components for onboarding page * saving work in progress * new fonts * [WIP] new onboarding page, initial page, components * WIP calendar connect * WIP availability new design * WIP onboarding page * WIP onboarding, working new availability form * WIP AvailabilitySchedule componente v2 * WIP availability with defaultSchedule * User profile view * Relocate new onboarding/getting-started page components * Steps test for onboarding v2 * Remove logs and unused code * remove any as types * Adding translations * Fixes translation text and css for step 4 * Deprecation note for old-getting-started * Added defaul events and refetch user query when finishing getting-started * Fix button text translation * Undo schedule v1 changes * Fix calendar switches state * Add cookie to save return-to when connecting calendar * Change useTranslation for useLocale instead * Change test to work with data-testid instead of hardcoded plain text due to translation * Fix skeleton containers for calendars * Style fixes * fix styles to match v2 * Fix styles and props types to match v2 design * Bugfix/router and console errors (#4206) * The loading={boolean} parameter is required, so this must be <Button /> * Fixes duplicate key error * Use zod and router.query.step directly to power state machine * use ul>li & divide for borders * Update apps/web/components/getting-started/steps-views/ConnectCalendars.tsx Co-authored-by: alannnc <alannnc@gmail.com> * Linting * Deprecation notices and type fixes * Update CreateEventsOnCalendarSelect.tsx * Type fixes Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: zomars <zomars@me.com>
2022-09-06 22:58:16 +00:00
// @@DEPRECATED, use new getting-started.tsx instead
import { zodResolver } from "@hookform/resolvers/zod";
import { Prisma } from "@prisma/client";
import classnames from "classnames";
import debounce from "lodash/debounce";
import omit from "lodash/omit";
import { NextPageContext } from "next";
import { useSession } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
2022-06-10 18:38:46 +00:00
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
App Store (#1869) * patch applied * patch applied * We shouldn't pollute global css * Build fixes * Updates typings * WIP extracting zoom to package * Revert "Upgrades next to 12.1 (#1895)" (#1903) This reverts commit ede0e98e1f7d462fe7196c6ce0de29490c00331e. * Tweak/gitignore prisma zod (#1905) * Extracts ignored createEventTypeBaseInput * Adds postinstall script * Revert "Tweak/gitignore prisma zod (#1905)" (#1906) This reverts commit 15bfeb30d7ce22a44f6dce9a74803a97ef43e2e6. * Eslint fixes (#1898) * Eslint fixes * Docs build fixes * Upgrade to next 12.1 (#1904) * Upgrades next to 12.1 * Fixes build * Updaters e2e test pipelines Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Fix URL by removing slash and backslash (#1733) * Fix URl by removing slash and backslash * Implement slugify * Add data type * Fixing folder structure * Solve zod-utils conflict * Build fixes (#1929) * Build fixes * Fixes type error * WIP * Conflict fixes * Removes unused file * TODO * WIP * Type fixes * Linting * WIP * Moved App definition to types * WIP * WIP * WIP * WIP WIP * Renamed zoomvideo app * Import fix * Daily.co app (#2022) * Daily.co app * Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts Co-authored-by: Omar López <zomars@me.com> * Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts Co-authored-by: Omar López <zomars@me.com> * Missing deps for newly added contants to lib Co-authored-by: Omar López <zomars@me.com> * WIP * WIP * WIP * Daily fixes * Updated type info * Slack Oauth integration - api route ideas * Adds getLocationOptions * Type fixes * Adds location option for daily video * Revert "Slack Oauth integration - api route ideas" This reverts commit 35ffa78e929339c4badb98cdab4e4b953ecc7cca. * Slack Oauth + verify sig * Revert "Slack Oauth + verify sig" This reverts commit ee95795e0f0ae6d06be4e0a423afb8c315d9af7d. * Huddle01 migration to app store (#2038) * Jitsi Video App migration * Removing uneeded dependencies * Missed unused reference * Missing dependency `@calcom/lib` is needed in the `locationOption.ts` file * Huddle01 migration to app store * Jitsi Video App migration (#2027) * Jitsi Video App migration * Removing uneeded dependencies * Missed unused reference * Missing dependency `@calcom/lib` is needed in the `locationOption.ts` file Co-authored-by: Omar López <zomars@me.com> * Monorepo/app store MS Teams Integration (#2080) * Create teamsvideo package * Remove zoom specific refrences * Add teams video files * Rename to office365_video * Add call back to add crednetial type office365_teams * Rename to office_video to match type * Add MS Teams as a location option * Rename files * Add teams reponse interface and create meeting * Comment out Daily imports * Add check for Teams integration * Add token checking functions * Change template to create event rather than meeting * Add comment to test between create link and event * Add teams URL to booking * Ask for just onlineMeeting permission * Add MS Teams logo * Add message to have an enterprise account * Remove comments * Comment back hasDailyIntegration * Comment back daily credentials * Update link to MS Graph section of README * Move API calls to package Co-authored-by: Omar López <zomars@me.com> * Re-adds missing module for transpiling * Adds email as required field for app store metadata * WIP: migrates tandem to app store * Cleanup * Migrates tandem api routes to app store * Fixes tandem api handlers * Big WIP WIP * Build fixes * WIP * Fixes annoying circular dependency bug I've spent a whole day on this.... * Location option cleanup * Type fixes * Update EventManager.ts * Update CalendarManager.ts * Moves CalendarService back to lib * Moves apple calendar to App Store * Cleanup * More cleanup * Migrates apple calendar * Returns all connected calendars credentials * No tsx needed in calcom/lib * Update auth.ts * Reordering * Update i18n.utils.ts * WIP: Google Meet * Type fixes * Type fixes * Cleanup * Update LinkIconButton.tsx * Update TrialBanner.tsx * Cleanup * Cleanup * Type fixes * Update _appRegistry.ts * Update fonts.css * Update CalEventParser.ts * Delete yarn.lock.rej * Update eslint-preset.js * Delete zoom.tsx * Type fixes * Migrates caldav to app store * Cleanup * Type fixes * Adds caldav to app store * Test fixes * Updates integration tests * Moar test fixes * Redirection fixes * Redirection fixes * Update timeFormat.ts * Update booking-pages.test.ts * Connect button fixes * Fix empty item * Cal fixes andrea (#2234) * Fixes #2178 * Fixes #2178 * Update apps/web/components/availability/Schedule.tsx * Update apps/web/components/availability/Schedule.tsx Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Peer Richelsen <peer@cal.com> * added meta viewport to disable zoom on input focus on mobile (#2238) * Update lint.yml (#2211) Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Fix prisma client bundle makes app slow (#2237) Co-authored-by: Omar López <zomars@me.com> * Slider fixes * Removed unused code * Full Shell when unauthed * App sidebar responsive fixes * Adds dynamic install button * Fix for duplicate connected calendars * Various fixes * Display notification on app delete * Reuse connect button * Adds CalDav button * Deprecates ConnectIntegration * Simplify install button * Adds Google Calendar connect button * Adds Office 365 Install button * Migrates Stripe to App Store * Zoom Install Button (#2244) * Fix minor css, app image load from static path * Fix app logos remote img src (#2252) * Adds missing exports * Cleanup * Disables install button for globally enabled apps * Update EventManager.ts * Stripe fixes * Disables example app Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Juan Esteban Nieto Cifuentes <89233604+Jenietoc@users.noreply.github.com> Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: Sean Brydon <seanbrydon.me@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: andreaestefania12 <andreaestefania12@hotmail.com> Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: Demian Caldelas <denik.works@protonmail.com> Co-authored-by: Alan <alannnc@gmail.com>
2022-03-23 22:00:30 +00:00
import getApps from "@calcom/app-store/utils";
2022-06-28 20:40:58 +00:00
import dayjs from "@calcom/dayjs";
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { DOCS_URL } from "@calcom/lib/constants";
feature/settings-username-update (#2306) * WIP feature/settings-username-update * WIP username change * WIP downgrade stripe * stripe downgrade and prorate preview * new UI for username premium component * Fix server side props * Remove migration, changed field to metadata user * WIP for update subscriptions * WIP intent username table * WIP saving and updating username via hooks * WIP saving working username sub update * WIP, update html to work with tests * Added stripe test for username update go to stripe * WIP username change test * Working test for username change * Fix timeout for flaky test * Review changes, remove logs * Move input username as a self contained component * Self review changes * Removing unnecesary arrow function * Removed intentUsername table and now using user metadata * Update website * Update turbo.json * Update e2e.yml * Update yarn.lock * Fixes for self host username update * Revert yarn lock from main branch * E2E fixes * Centralizes username check * Improvements * WIP separate logic between premium and save username button * WIP refactor username premium update * Saving WIP * WIP redo of username check * WIP obtain action normal, update or downgrade * Update username change components * Fix test for change-username self host or cal server * Fix user type for premiumTextfield * Using now a global unique const to know if is selfhosted, css fixes * Remove unused import * Using dynamic import for username textfield, prevent submit on enter Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-06 19:31:07 +00:00
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
2022-07-27 02:24:00 +00:00
import { Icon } from "@calcom/ui/Icon";
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import { Form } from "@calcom/ui/form/fields";
import { getSession } from "@lib/auth";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { Schedule as ScheduleType } from "@lib/types/schedule";
import { ClientSuspense } from "@components/ClientSuspense";
import Loader from "@components/Loader";
import Schedule from "@components/availability/Schedule";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
import { TRPCClientErrorLike } from "@trpc/client";
// Embed isn't applicable to onboarding, so ignore the rule
/* eslint-disable @calcom/eslint/avoid-web-storage */
Feature/new onboarding page (#3377) * [WIP] New design and components for onboarding page * saving work in progress * new fonts * [WIP] new onboarding page, initial page, components * WIP calendar connect * WIP availability new design * WIP onboarding page * WIP onboarding, working new availability form * WIP AvailabilitySchedule componente v2 * WIP availability with defaultSchedule * User profile view * Relocate new onboarding/getting-started page components * Steps test for onboarding v2 * Remove logs and unused code * remove any as types * Adding translations * Fixes translation text and css for step 4 * Deprecation note for old-getting-started * Added defaul events and refetch user query when finishing getting-started * Fix button text translation * Undo schedule v1 changes * Fix calendar switches state * Add cookie to save return-to when connecting calendar * Change useTranslation for useLocale instead * Change test to work with data-testid instead of hardcoded plain text due to translation * Fix skeleton containers for calendars * Style fixes * fix styles to match v2 * Fix styles and props types to match v2 design * Bugfix/router and console errors (#4206) * The loading={boolean} parameter is required, so this must be <Button /> * Fixes duplicate key error * Use zod and router.query.step directly to power state machine * use ul>li & divide for borders * Update apps/web/components/getting-started/steps-views/ConnectCalendars.tsx Co-authored-by: alannnc <alannnc@gmail.com> * Linting * Deprecation notices and type fixes * Update CreateEventsOnCalendarSelect.tsx * Type fixes Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: zomars <zomars@me.com>
2022-09-06 22:58:16 +00:00
export interface ScheduleFormValues {
schedule: ScheduleType;
Feature/new onboarding page (#3377) * [WIP] New design and components for onboarding page * saving work in progress * new fonts * [WIP] new onboarding page, initial page, components * WIP calendar connect * WIP availability new design * WIP onboarding page * WIP onboarding, working new availability form * WIP AvailabilitySchedule componente v2 * WIP availability with defaultSchedule * User profile view * Relocate new onboarding/getting-started page components * Steps test for onboarding v2 * Remove logs and unused code * remove any as types * Adding translations * Fixes translation text and css for step 4 * Deprecation note for old-getting-started * Added defaul events and refetch user query when finishing getting-started * Fix button text translation * Undo schedule v1 changes * Fix calendar switches state * Add cookie to save return-to when connecting calendar * Change useTranslation for useLocale instead * Change test to work with data-testid instead of hardcoded plain text due to translation * Fix skeleton containers for calendars * Style fixes * fix styles to match v2 * Fix styles and props types to match v2 design * Bugfix/router and console errors (#4206) * The loading={boolean} parameter is required, so this must be <Button /> * Fixes duplicate key error * Use zod and router.query.step directly to power state machine * use ul>li & divide for borders * Update apps/web/components/getting-started/steps-views/ConnectCalendars.tsx Co-authored-by: alannnc <alannnc@gmail.com> * Linting * Deprecation notices and type fixes * Update CreateEventsOnCalendarSelect.tsx * Type fixes Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: zomars <zomars@me.com>
2022-09-06 22:58:16 +00:00
}
let mutationComplete: ((err: Error | null) => void) | null;
export default function Onboarding(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const { user } = props;
const router = useRouter();
const utils = trpc.useContext();
const telemetry = useTelemetry();
const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const { data: eventTypes } = trpc.useQuery(["viewer.eventTypes.list"]);
const onSuccessMutation = async () => {
showToast(t("your_user_profile_updated_successfully"), "success");
setHasErrors(false); // dismiss any open errors
await utils.invalidateQueries(["viewer.me"]);
};
const onErrorMutation = (error: TRPCClientErrorLike<AppRouter>) => {
setHasErrors(true);
setErrorMessage(error.message);
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
};
const mutation = trpc.useMutation("viewer.updateProfile", {
onSuccess: async () => {
setSubmitting(true);
setEnteredName(nameRef.current?.value || "");
setInputUsernameValue(usernameRef.current?.value || "");
if (mutationComplete) {
mutationComplete(null);
mutationComplete = null;
}
setSubmitting(false);
},
onError: (err: TRPCClientErrorLike<AppRouter>) => {
setError(new Error(err.message));
if (mutationComplete) {
mutationComplete(new Error(err.message));
}
setSubmitting(false);
},
});
const DEFAULT_EVENT_TYPES = [
{
title: t("15min_meeting"),
slug: "15min",
length: 15,
},
{
title: t("30min_meeting"),
slug: "30min",
length: 30,
},
{
title: t("secret_meeting"),
slug: "secret",
length: 15,
hidden: true,
},
];
const [isSubmitting, setSubmitting] = React.useState(false);
const [enteredName, setEnteredName] = React.useState("");
const [currentUsername, setCurrentUsername] = useState(user.username || undefined);
const [inputUsernameValue, setInputUsernameValue] = useState(currentUsername);
const { status } = useSession();
const loading = status === "loading";
const [ready, setReady] = useState(false);
const [selectedImport, setSelectedImport] = useState("");
const [error, setError] = useState<Error | null>(null);
const updateUser = useCallback(
async (data: Prisma.UserUpdateInput) => {
const res = await fetch(`/api/user/${user.id}`, {
method: "PATCH",
body: JSON.stringify({ data: { ...data } }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error((await res.json()).message);
}
const responseData = await res.json();
return responseData.data;
},
[user.id]
);
2022-06-10 18:38:46 +00:00
const createEventType = trpc.useMutation("viewer.eventTypes.create");
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", {
onError: (error: TRPCClientErrorLike<AppRouter>) => {
throw new Error(error.message);
},
});
/** Name */
const nameRef = useRef<HTMLInputElement>(null);
/** Username */
const usernameRef = useRef<HTMLInputElement>(null!);
const bioRef = useRef<HTMLInputElement>(null);
/** End Name */
/** TimeZone */
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
/** End TimeZone */
/** Onboarding Steps */
const [currentStep, setCurrentStep] = useState(props.initialStep);
const handleConfirmStep = async () => {
try {
setSubmitting(true);
const onComplete = steps[currentStep]?.onComplete;
if (onComplete) {
await onComplete();
}
incrementStep();
setSubmitting(false);
} catch (error) {
setSubmitting(false);
setError(error as Error);
}
};
const debouncedHandleConfirmStep = debounce(handleConfirmStep, 850);
const handleSkipStep = () => {
incrementStep();
};
const incrementStep = () => {
const nextStep = currentStep + 1;
if (nextStep >= steps.length) {
completeOnboarding();
return;
}
setCurrentStep(nextStep);
};
const decrementStep = () => {
const previous = currentStep - 1;
if (previous < 0) {
return;
}
setCurrentStep(previous);
};
const goToStep = (step: number) => {
setCurrentStep(step);
};
/**
* Complete Onboarding finalizes the onboarding flow for a new user.
*
* Here, 3 event types are pre-created for the user as well.
* Set to the availability the user enter during the onboarding.
*
* If a user skips through the Onboarding flow,
* then the default availability is applied.
*/
const completeOnboarding = async () => {
setSubmitting(true);
if (eventTypes?.length === 0) {
await Promise.all(
DEFAULT_EVENT_TYPES.map(async (event) => {
return createEventType.mutate(event);
})
);
}
await updateUser({
completedOnboarding: true,
});
setSubmitting(false);
router.push("/event-types");
};
const schema = z.object({
token: z.string(),
});
const formMethods = useForm<{
token: string;
}>({ resolver: zodResolver(schema), mode: "onSubmit" });
// Should update username on user when being redirected from sign up and doing google/saml
useEffect(() => {
async function validateAndSave(username: string) {
const { data } = await fetchUsername(username);
// Only persist username if its available and not premium
// premium usernames are saved via stripe webhook
if (data.available && !data.premium) {
await updateUser({
username,
});
}
// Remove it from localStorage
window.localStorage.removeItem("username");
return;
}
// Looking for username on localStorage
const username = window.localStorage.getItem("username");
if (username) {
validateAndSave(username);
}
}, [updateUser]);
const availabilityForm = useForm({ defaultValues: { schedule: DEFAULT_SCHEDULE } });
const steps = [
{
id: t("welcome"),
title: t("welcome_to_calcom"),
description: t("welcome_instructions"),
Component: (
<>
{/** @NOTE: Hiding temporarily
* @URL related: https://github.com/calcom/cal.com/issues/3941
*/}
{/* {selectedImport == "" && (
<div className="mb-4 grid grid-cols-2 gap-x-4">
<Button color="secondary" onClick={() => setSelectedImport("calendly")}>
{t("import_from")} Calendly
</Button>
<Button color="secondary" onClick={() => setSelectedImport("savvycal")}>
{t("import_from")} SavvyCal
</Button>
</div>
)} */}
{/* {selectedImport && (
<div>
<h2 className="font-cal text-2xl text-gray-900">
{t("import_from")} {selectedImport === "calendly" ? "Calendly" : "SavvyCal"}
</h2>
2022-01-31 10:18:12 +00:00
<p className="mb-2 text-sm text-gray-500">
{t("you_will_need_to_generate")}. Find out how to do this{" "}
<a href={`${DOCS_URL}/import`}>here</a>.
2022-01-31 10:18:12 +00:00
</p>
<form
className="flex"
onSubmit={formMethods.handleSubmit(async (values) => {
// track the number of imports. Without personal data/payload
telemetry.event(telemetryEventTypes.importSubmitted, {
...collectPageParameters(),
selectedImport,
});
setSubmitting(true);
const response = await fetch(`/api/import/${selectedImport}`, {
method: "POST",
body: JSON.stringify({
token: values.token,
}),
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 201) {
setSubmitting(false);
handleSkipStep();
} else {
await response.json().catch((e) => {
console.log("Error: response.json invalid: " + e);
setSubmitting(false);
});
}
})}>
{hasErrors && <Alert severity="error" title={errorMessage} />}
<input
onChange={async (e) => {
formMethods.setValue("token", e.target.value);
}}
type="text"
name="token"
id="token"
placeholder={t("access_token")}
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
/>
<Button type="submit" className="mt-1 ml-4 h-10">
{t("import")}
</Button>
</form>
</div>
)}
<div className="relative my-4">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-white px-2 text-sm text-gray-500">or</span>
</div>
</div>
*/}
<form className="sm:mx-auto sm:w-full">
<section className="space-y-8">
<fieldset>
{user.username !== "" && (
<UsernameAvailability
currentUsername={currentUsername}
setCurrentUsername={setCurrentUsername}
inputUsernameValue={inputUsernameValue}
usernameRef={usernameRef}
setInputUsernameValue={setInputUsernameValue}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
user={user}
/>
)}
</fieldset>
<fieldset>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
{t("full_name")}
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
autoComplete="given-name"
placeholder={t("your_name")}
defaultValue={user.name ?? enteredName}
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
/>
</fieldset>
<fieldset>
<section className="flex justify-between">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<p className="text-sm leading-tight text-gray-500 dark:text-white">
{t("current_time")}:&nbsp;
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
</p>
</section>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={({ value }) => setSelectedTimeZone(value)}
className="mt-1 block w-full rounded-md border-gray-300 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
/>
</fieldset>
</section>
</form>
</>
),
hideConfirm: false,
confirmText: t("continue"),
showCancel: true,
cancelText: t("set_up_later"),
onComplete: async () => {
mutationComplete = null;
setError(null);
const mutationAsync = new Promise((resolve, reject) => {
mutationComplete = (err) => {
if (err) {
reject(err);
return;
}
resolve(null);
};
});
const userUpdateData = {
name: nameRef.current?.value,
username: usernameRef.current?.value,
timeZone: selectedTimeZone,
};
mutation.mutate(userUpdateData);
if (mutationComplete) {
await mutationAsync;
}
},
},
{
id: "connect-calendar",
title: t("connect_your_calendar"),
description: t("connect_your_calendar_instructions"),
Component: (
<section>
<ClientSuspense fallback={<Loader />}>
<CalendarListContainer heading={false} fromOnboarding={true} />
</ClientSuspense>
<footer className="flex flex-col space-y-6 py-6 sm:mx-auto sm:w-full">
<Button
className="justify-center"
EndIcon={Icon.FiArrowRight}
type="button"
onClick={handleConfirmStep}>
{t("continue")}
</Button>
</footer>
</section>
),
hideConfirm: true,
confirmText: t("continue"),
showCancel: true,
cancelText: t("continue_without_calendar"),
},
{
id: "set-availability",
title: t("set_availability"),
description: t("set_availability_instructions"),
Component: (
<Form<ScheduleFormValues>
className="mx-auto max-w-lg bg-white text-black dark:bg-opacity-5 dark:text-white"
form={availabilityForm}
handleSubmit={async (values) => {
try {
setSubmitting(true);
await createSchedule.mutate({
name: t("default_schedule_name"),
...values,
});
debouncedHandleConfirmStep();
setSubmitting(false);
} catch (error) {
if (error instanceof Error) {
setError(error);
}
}
}}>
<section>
<Schedule name="schedule" />
<footer className="flex flex-col space-y-6 py-6 sm:mx-auto sm:w-full">
<Button className="justify-center" EndIcon={Icon.FiArrowRight} type="submit">
{t("continue")}
</Button>
</footer>
</section>
</Form>
),
hideConfirm: true,
showCancel: false,
},
{
id: "profile",
title: t("nearly_there"),
description: t("nearly_there_instructions"),
Component: (
2021-10-09 13:34:13 +00:00
<form className="sm:mx-auto sm:w-full" id="ONBOARDING_STEP_4">
<section className="space-y-4">
<fieldset>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
{t("full_name")}
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
autoComplete="given-name"
placeholder={t("your_name")}
defaultValue={user.name || enteredName}
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
/>
</fieldset>
<fieldset>
<label htmlFor="bio" className="block text-sm font-medium text-gray-700">
{t("about")}
</label>
<input
ref={bioRef}
type="text"
name="bio"
id="bio"
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
defaultValue={user.bio || undefined}
/>
<p className="mt-2 text-sm leading-tight text-gray-500 dark:text-white">
{t("few_sentences_about_yourself")}
</p>
</fieldset>
</section>
</form>
),
hideConfirm: false,
confirmText: t("finish"),
showCancel: true,
cancelText: t("set_up_later"),
onComplete: async () => {
try {
setSubmitting(true);
await updateUser({
bio: bioRef.current?.value,
});
setSubmitting(false);
} catch (error) {
setError(error as Error);
setSubmitting(false);
}
},
},
];
/** End Onboarding Steps */
useEffect(() => {
setReady(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (loading || !ready) {
2022-07-13 21:14:16 +00:00
return <div className="loader" />;
}
return (
2022-02-09 22:32:31 +00:00
<div className="bg-brand min-h-screen" data-testid="onboarding">
<Head>
<title>Cal.com - {t("getting_started")}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{isSubmitting && (
<div className="fixed z-10 flex h-full w-full flex-col content-center items-center justify-center bg-white bg-opacity-25">
<Loader />
</div>
)}
<div className="mx-auto px-4 py-24">
<article className="relative">
<section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg">
<header>
<p className="font-cal mb-2 text-3xl tracking-wider text-white">{steps[currentStep].title}</p>
<p className="text-sm font-normal text-white">{steps[currentStep].description}</p>
</header>
<section className="space-y-2 pt-4">
<p className="text-xs font-medium text-gray-500 dark:text-white">
Step {currentStep + 1} of {steps.length}
</p>
{error && <Alert severity="error" message={error?.message} />}
<section className="flex w-full space-x-2 rtl:space-x-reverse">
{steps.map((s, index) => {
return index <= currentStep ? (
<div
key={`step-${index}`}
onClick={() => goToStep(index)}
className={classnames(
"h-1 w-1/4 bg-white",
index < currentStep ? "cursor-pointer" : ""
2022-07-13 21:14:16 +00:00
)}
/>
) : (
2022-07-13 21:14:16 +00:00
<div key={`step-${index}`} className="h-1 w-1/4 bg-white bg-opacity-25" />
);
})}
</section>
</section>
</section>
<section className="mx-auto mt-10 max-w-xl rounded-sm bg-white p-10">
{steps[currentStep].Component}
{!steps[currentStep].hideConfirm && (
<footer className="mt-8 flex flex-col space-y-6 sm:mx-auto sm:w-full">
<Button
className="justify-center"
disabled={isSubmitting}
onClick={debouncedHandleConfirmStep}
EndIcon={Icon.FiArrowRight}
data-testid={`continue-button-${currentStep}`}>
{steps[currentStep].confirmText}
</Button>
</footer>
)}
</section>
<section className="mx-auto max-w-xl py-8">
<div className="flex flex-row-reverse justify-between">
<button
disabled={isSubmitting}
onClick={handleSkipStep}
className="text-sm leading-tight text-gray-500 dark:text-white">
{t("next_step")}
</button>
{currentStep !== 0 && (
<button
disabled={isSubmitting}
onClick={decrementStep}
className="text-sm leading-tight text-gray-500 dark:text-white">
{t("prev_step")}
</button>
)}
</div>
</section>
</article>
</div>
</div>
);
}
export async function getServerSideProps(context: NextPageContext) {
const session = await getSession(context);
if (!session?.user?.id) {
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
};
}
const user = await prisma.user.findFirst({
where: {
id: session.user.id,
},
select: {
id: true,
startTime: true,
endTime: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
timeZone: true,
identityProvider: true,
completedOnboarding: true,
weekStart: true,
hideBranding: true,
theme: true,
plan: true,
brandColor: true,
darkBrandColor: true,
metadata: true,
timeFormat: true,
allowDynamicBooking: true,
selectedCalendars: {
select: {
externalId: true,
integration: true,
},
},
credentials: {
where: {
userId: session.user.id,
},
select: {
id: true,
type: true,
key: true,
userId: true,
appId: true,
},
},
schedules: {
where: {
userId: session.user.id,
},
select: {
id: true,
},
},
},
});
if (!user) {
throw new Error(`Signed in as ${session.user.id} but cannot be found in db`);
}
if (user.completedOnboarding) {
return {
redirect: {
permanent: false,
destination: "/event-types",
},
};
}
const integrations = getApps(user.credentials)
.filter((item) => item.type.endsWith("_calendar"))
.map((item) => omit(item, "key"));
const { schedules } = user;
const hasConfigureCalendar = integrations.some((integration) => integration.credential !== null);
const hasSchedules = schedules && schedules.length > 0;
return {
props: {
session,
user,
initialStep: hasSchedules ? (hasConfigureCalendar ? 2 : 3) : 0,
},
};
}