add free plan (#549)

- add new fields to support this
- when free:
  - fade out all event types after first
  - hide events after first on booking page
  - make booking page after the first one 404 if accessed directly
- add e2e tests
This commit is contained in:
Alex Johansson 2021-09-06 15:51:15 +02:00 committed by GitHub
parent fa35af7bd8
commit 7e6e935ed3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 880 additions and 261 deletions

View file

@ -66,3 +66,4 @@ jobs:
name: videos name: videos
path: | path: |
cypress/videos cypress/videos
cypress/screenshots

42
components/ui/Alert.tsx Normal file
View file

@ -0,0 +1,42 @@
import { XCircleIcon, InformationCircleIcon, CheckCircleIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { ReactNode } from "react";
export interface AlertProps {
title: ReactNode;
message?: ReactNode;
className?: string;
severity: "success" | "warning" | "error";
}
export function Alert(props: AlertProps) {
const { severity } = props;
return (
<div
className={classNames(
"rounded-md p-4",
props.className,
severity === "error" && "bg-red-50 text-red-800",
severity === "warning" && "bg-yellow-50 text-yellow-800",
severity === "success" && "bg-gray-900 text-white"
)}>
<div className="flex">
<div className="flex-shrink-0">
{severity === "error" && (
<XCircleIcon className={classNames("h-5 w-5 text-red-400")} aria-hidden="true" />
)}
{severity === "warning" && (
<InformationCircleIcon className={classNames("h-5 w-5 text-yellow-400")} aria-hidden="true" />
)}
{severity === "success" && (
<CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
)}
</div>
<div className="ml-3">
<h3 className="text-sm font-medium">{props.title}</h3>
<div className="text-sm">{props.message}</div>
</div>
</div>
</div>
);
}

View file

@ -68,7 +68,7 @@ export const Button = function Button(props: ButtonProps) {
? "text-gray-400 bg-transparent" ? "text-gray-400 bg-transparent"
: "text-red-700 bg-transparent hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-50 focus:ring-red-500"), : "text-red-700 bg-transparent hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-50 focus:ring-red-500"),
// set not-allowed cursor if disabled // set not-allowed cursor if disabled
disabled && "cursor-not-allowed", loading ? "cursor-wait" : disabled ? "cursor-not-allowed" : "",
props.className props.className
), ),
// if we click a disabled button, we prevent going through the click handler // if we click a disabled button, we prevent going through the click handler

View file

@ -1,19 +1,8 @@
import { XCircleIcon } from "@heroicons/react/solid"; import { Alert } from "../Alert";
export default function ErrorAlert(props) { /**
return ( * @deprecated use `<Alert severity="error" message="x" />` instead
<div className="rounded-md bg-red-50 p-4"> */
<div className="flex"> export default function ErrorAlert(props: { message: string; className?: string }) {
<div className="flex-shrink-0"> return <Alert severity="errror" message={props.message} />;
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Something went wrong</h3>
<div className="text-sm text-red-700">
<p>{props.message}</p>
</div>
</div>
</div>
</div>
);
} }

View file

@ -1,3 +1,4 @@
{ {
"baseUrl": "http://localhost:3000" "baseUrl": "http://localhost:3000",
"chromeWebSecurity": false
} }

View file

@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
describe("booking pages", () => {
describe("free user", () => {
it("only one visibile event", () => {
cy.visit("/free");
cy.get("[data-testid=event-types]").children().should("have.length", 1);
cy.get('[href="/free/30min"]').should("exist");
cy.get('[href="/free/60min"]').should("not.exist");
});
it("/free/30min is bookable", () => {
cy.request({
method: "GET",
url: "/free/30min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(200);
});
});
it("/free/60min is not bookable", () => {
cy.request({
method: "GET",
url: "/free/60min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(404);
});
});
});
it("pro user's page has at least 2 visibile events", () => {
cy.visit("/pro");
cy.get("[data-testid=event-types]").children().should("have.length.at.least", 2);
});
describe("free user with first hidden", () => {
it("has no visible events", () => {
cy.visit("/free-first-hidden");
cy.contains("This user hasn't set up any event types yet.");
});
it("/free-first-hidden/30min is not bookable", () => {
cy.request({
method: "GET",
url: "/free-first-hidden/30min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(404);
});
});
it("/free-first-hidden/60min is not bookable", () => {
cy.request({
method: "GET",
url: "/free-first-hidden/60min",
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.eql(404);
});
});
});
});

View file

@ -0,0 +1,66 @@
function randomString(length: number) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
describe("pro user", () => {
before(() => {
cy.visit("/event-types");
cy.login("pro@example.com", "pro");
});
beforeEach(() => {
cy.visit("/event-types");
});
it("has at least 2 events", () => {
cy.get("[data-testid=event-types]").children().should("have.length.at.least", 2);
cy.get("[data-testid=event-types]")
.children()
.each(($el) => {
expect($el).to.have.attr("data-disabled", "0");
});
});
it("can add new event type", () => {
cy.get("[data-testid=new-event-type]").click();
const nonce = randomString(3);
const eventTitle = `hello ${nonce}`;
cy.get("[name=title]").focus().type(eventTitle);
cy.get("[name=length]").focus().type("10");
cy.get("[type=submit]").click();
cy.location("pathname").should("not.eq", "/event-types");
cy.visit("/event-types");
cy.get("[data-testid=event-types]").contains(eventTitle);
});
});
describe("free user", () => {
before(() => {
cy.visit("/event-types");
cy.login("free@example.com", "free");
});
describe("/event-types", () => {
beforeEach(() => {
cy.visit("/event-types");
});
it("has at least 2 events where first is enabled", () => {
cy.get("[data-testid=event-types]").children().should("have.length.at.least", 2);
cy.get("[data-testid=event-types]").children().first().should("have.attr", "data-disabled", "0");
cy.get("[data-testid=event-types]").children().last().should("have.attr", "data-disabled", "1");
});
it("can not add new event type", () => {
cy.get("[data-testid=new-event-type]").should("be.disabled");
});
});
});

View file

@ -1,6 +1,4 @@
/// <reference types="cypress" /> describe("smoke test", () => {
describe("silly test", () => {
it("loads /", () => { it("loads /", () => {
cy.visit("/"); cy.visit("/");
cy.contains("Sign in to your account"); cy.contains("Sign in to your account");

View file

@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View file

@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable;
}
}
}
Cypress.Commands.add("login", (email: string, password: string) => {
cy.log(` 🗝 Logging in with ${email}`);
Cypress.Cookies.defaults({
preserve: /next-auth/,
});
cy.clearCookies();
cy.clearCookie("next-auth.session-token");
cy.reload();
cy.get("[name=email]").focus().clear().type(email);
cy.get("[name=password]").focus().clear().type(password);
cy.get("[type=submit]").click();
cy.wait(500);
});
export {};

View file

@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

4
cypress/support/index.ts Normal file
View file

@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-namespace */
import "./commands";

10
cypress/tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

View file

@ -1,3 +1,11 @@
export function asStringOrNull(str: unknown) { export function asStringOrNull(str: unknown) {
return typeof str === "string" ? str : null; return typeof str === "string" ? str : null;
} }
export function asStringOrThrow(str: unknown): string {
const type = typeof str;
if (type !== "string") {
throw new Error(`Expected "string" - got ${type}`);
}
return str;
}

View file

@ -0,0 +1,31 @@
import { useRouter } from "next/router";
import { useMemo } from "react";
export function useToggleQuery(name: string) {
const router = useRouter();
const hrefOff = useMemo(() => {
const query = {
...router.query,
};
delete query[name];
return {
query,
};
}, [router.query, name]);
const hrefOn = useMemo(() => {
const query = {
...router.query,
[name]: "1",
};
return {
query,
};
}, [router.query, name]);
return {
hrefOn,
hrefOff,
isOn: router.query[name] === "1",
};
}

View file

@ -7,7 +7,9 @@ if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient(); prisma = new PrismaClient();
} else { } else {
if (!globalAny.prisma) { if (!globalAny.prisma) {
globalAny.prisma = new PrismaClient(); globalAny.prisma = new PrismaClient({
log: ["query", "error", "warn"],
});
} }
prisma = globalAny.prisma; prisma = globalAny.prisma;
} }

View file

@ -2,7 +2,7 @@
type GetSSRResult<TProps> = type GetSSRResult<TProps> =
// //
{ props: TProps } | { redirect: any } | { notFound: true }; { props: TProps } | { redirect: any } | { notFound: boolean };
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>; type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;

View file

@ -8,7 +8,7 @@
"db-migrate": "yarn prisma migrate dev", "db-migrate": "yarn prisma migrate dev",
"db-seed": "yarn ts-node scripts/seed.ts", "db-seed": "yarn ts-node scripts/seed.ts",
"db-nuke": "docker-compose down --volumes --remove-orphans", "db-nuke": "docker-compose down --volumes --remove-orphans",
"dx": "DATABASE_URL=postgresql://postgres:@localhost:5450/calendso eval 'yarn db-up && yarn prisma migrate dev && yarn db-seed && yarn dev'", "dx": "BASE_URL='http://localhost:3000' DATABASE_URL=postgresql://postgres:@localhost:5450/calendso eval 'yarn db-up && yarn prisma migrate dev && yarn db-seed && yarn dev'",
"test": "node node_modules/.bin/jest", "test": "node node_modules/.bin/jest",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
@ -51,6 +51,7 @@
"next-seo": "^4.26.0", "next-seo": "^4.26.0",
"next-transpile-modules": "^8.0.0", "next-transpile-modules": "^8.0.0",
"nodemailer": "^6.6.3", "nodemailer": "^6.6.3",
"npm-run-all": "^4.1.5",
"react": "17.0.2", "react": "17.0.2",
"react-dates": "^21.8.0", "react-dates": "^21.8.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
@ -73,6 +74,7 @@
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/react": "^17.0.18", "@types/react": "^17.0.18",
"@types/react-dates": "^21.8.3", "@types/react-dates": "^21.8.3",
"@types/react-select": "^4.0.17",
"@typescript-eslint/eslint-plugin": "^4.30.0", "@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.29.2", "@typescript-eslint/parser": "^4.29.2",
"autoprefixer": "^10.3.1", "autoprefixer": "^10.3.1",
@ -91,7 +93,7 @@
"prisma": "^2.30.2", "prisma": "^2.30.2",
"tailwindcss": "^2.2.7", "tailwindcss": "^2.2.7",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.3.5" "typescript": "^4.4.2"
}, },
"lint-staged": { "lint-staged": {
"./{*,{pages,components,lib}/**/*}.{js,ts,jsx,tsx}": [ "./{*,{pages,components,lib}/**/*}.{js,ts,jsx,tsx}": [

View file

@ -1,17 +1,41 @@
import { GetServerSideProps } from "next";
import { HeadSeo } from "@components/seo/head-seo";
import Link from "next/link";
import prisma, { whereAndSelect } from "@lib/prisma";
import Avatar from "@components/Avatar"; import Avatar from "@components/Avatar";
import { HeadSeo } from "@components/seo/head-seo";
import Theme from "@components/Theme"; import Theme from "@components/Theme";
import { ClockIcon, InformationCircleIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import { ArrowRightIcon } from "@heroicons/react/outline"; import { ArrowRightIcon } from "@heroicons/react/outline";
import { ClockIcon, InformationCircleIcon, UserIcon } from "@heroicons/react/solid";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { GetServerSidePropsContext } from "next";
import Link from "next/link";
import React from "react";
export default function User(props): User { export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const { isReady } = Theme(props.user.theme); const { isReady } = Theme(props.user.theme);
const eventTypes = props.eventTypes.map((type) => ( return (
<>
<HeadSeo
title={props.user.name || props.user.username}
description={props.user.name || props.user.username}
name={props.user.name || props.user.username}
avatar={props.user.avatar}
/>
{isReady && (
<div className="bg-neutral-50 dark:bg-black h-screen">
<main className="max-w-3xl mx-auto py-24 px-4">
<div className="mb-8 text-center">
<Avatar
imageSrc={props.user.avatar}
displayName={props.user.name}
className="mx-auto w-24 h-24 rounded-full mb-4"
/>
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white mb-1">
{props.user.name || props.user.username}
</h1>
<p className="text-neutral-500 dark:text-white">{props.user.bio}</p>
</div>
<div className="space-y-6" data-testid="event-types">
{props.eventTypes.map((type) => (
<div <div
key={type.id} key={type.id}
className="group relative dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 bg-white hover:bg-gray-50 border border-neutral-200 hover:border-black rounded-sm"> className="group relative dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 bg-white hover:bg-gray-50 border border-neutral-200 hover:border-black rounded-sm">
@ -45,31 +69,9 @@ export default function User(props): User {
</a> </a>
</Link> </Link>
</div> </div>
)); ))}
return (
<>
<HeadSeo
title={props.user.name || props.user.username}
description={props.user.name || props.user.username}
name={props.user.name || props.user.username}
avatar={props.user.avatar}
/>
{isReady && (
<div className="bg-neutral-50 dark:bg-black h-screen">
<main className="max-w-3xl mx-auto py-24 px-4">
<div className="mb-8 text-center">
<Avatar
imageSrc={props.user.avatar}
displayName={props.user.name}
className="mx-auto w-24 h-24 rounded-full mb-4"
/>
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white mb-1">
{props.user.name || props.user.username}
</h1>
<p className="text-neutral-500 dark:text-white">{props.user.bio}</p>
</div> </div>
<div className="space-y-6">{eventTypes}</div> {props.eventTypes.length == 0 && (
{eventTypes.length == 0 && (
<div className="shadow overflow-hidden rounded-sm"> <div className="shadow overflow-hidden rounded-sm">
<div className="p-8 text-center text-gray-400 dark:text-white"> <div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-semibold text-3xl text-gray-600 dark:text-white">Uh oh!</h2> <h2 className="font-semibold text-3xl text-gray-600 dark:text-white">Uh oh!</h2>
@ -84,33 +86,45 @@ export default function User(props): User {
); );
} }
export const getServerSideProps: GetServerSideProps = async (context) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const user = await whereAndSelect( const username = (context.query.user as string).toLowerCase();
prisma.user.findFirst,
{ const user = await prisma.user.findUnique({
username: context.query.user.toLowerCase(), where: {
username,
}, },
["id", "username", "email", "name", "bio", "avatar", "theme"] select: {
); id: true,
username: true,
email: true,
name: true,
bio: true,
avatar: true,
theme: true,
plan: true,
},
});
if (!user) { if (!user) {
return { return {
notFound: true, notFound: true,
}; };
} }
const eventTypes = await prisma.eventType.findMany({ const eventTypesWithHidden = await prisma.eventType.findMany({
where: { where: {
userId: user.id, userId: user.id,
hidden: false,
}, },
select: { select: {
id: true,
slug: true, slug: true,
title: true, title: true,
length: true, length: true,
description: true, description: true,
hidden: true,
}, },
take: user.plan === "FREE" ? 1 : undefined,
}); });
const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
return { return {
props: { props: {
user, user,

View file

@ -235,6 +235,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
availability: true, availability: true,
hideBranding: true, hideBranding: true,
theme: true, theme: true,
plan: true,
}, },
}); });
@ -243,12 +244,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true, notFound: true,
} as const; } as const;
} }
const eventType = await prisma.eventType.findUnique({
const eventType = await prisma.eventType.findFirst({
where: { where: {
userId_slug: {
userId: user.id, userId: user.id,
slug: typeParam, slug: typeParam,
}, },
},
select: { select: {
id: true, id: true,
title: true, title: true,
@ -262,15 +264,32 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
periodEndDate: true, periodEndDate: true,
periodCountCalendarDays: true, periodCountCalendarDays: true,
minimumBookingNotice: true, minimumBookingNotice: true,
hidden: true,
}, },
}); });
if (!eventType) { if (!eventType || eventType.hidden) {
return { return {
notFound: true, notFound: true,
} as const; } as const;
} }
// check this is the first event
if (user.plan === "FREE") {
const firstEventType = await prisma.eventType.findFirst({
where: {
userId: user.id,
},
select: {
id: true,
},
});
if (firstEventType?.id !== eventType.id) {
return {
notFound: true,
} as const;
}
}
const getWorkingHours = (providesAvailability: { availability: Availability[] }) => const getWorkingHours = (providesAvailability: { availability: Availability[] }) =>
providesAvailability.availability && providesAvailability.availability.length providesAvailability.availability && providesAvailability.availability.length
? providesAvailability.availability ? providesAvailability.availability

View file

@ -1,8 +1,7 @@
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Modal from "@components/Modal"; import Modal from "@components/Modal";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import Select, { OptionBase } from "react-select"; import Select, { OptionTypeBase } from "react-select";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client"; import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import { LocationType } from "@lib/location"; import { LocationType } from "@lib/location";
@ -25,7 +24,7 @@ import {
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import { Availability, EventType, User } from "@prisma/client"; import { Availability } from "@prisma/client";
import { validJson } from "@lib/jsonUtils"; import { validJson } from "@lib/jsonUtils";
import classnames from "classnames"; import classnames from "classnames";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
@ -42,6 +41,8 @@ import updateEventType from "@lib/mutations/event-types/update-event-type";
import deleteEventType from "@lib/mutations/event-types/delete-event-type"; import deleteEventType from "@lib/mutations/event-types/delete-event-type";
import showToast from "@lib/notification"; import showToast from "@lib/notification";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { asStringOrThrow } from "@lib/asStringOrNull";
import Button from "@components/ui/Button";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -66,7 +67,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const router = useRouter(); const router = useRouter();
const [successModalOpen, setSuccessModalOpen] = useState(false); const [successModalOpen, setSuccessModalOpen] = useState(false);
const inputOptions: OptionBase[] = [ const inputOptions: OptionTypeBase[] = [
{ value: EventTypeCustomInputType.TEXT, label: "Text" }, { value: EventTypeCustomInputType.TEXT, label: "Text" },
{ value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" }, { value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" },
{ value: EventTypeCustomInputType.NUMBER, label: "Number" }, { value: EventTypeCustomInputType.NUMBER, label: "Number" },
@ -130,8 +131,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [showLocationModal, setShowLocationModal] = useState(false); const [showLocationModal, setShowLocationModal] = useState(false);
const [showAddCustomModal, setShowAddCustomModal] = useState(false); const [showAddCustomModal, setShowAddCustomModal] = useState(false);
const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [selectedLocation, setSelectedLocation] = useState<OptionBase | undefined>(undefined); const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [selectedInputOption, setSelectedInputOption] = useState<OptionBase>(inputOptions[0]); const [selectedInputOption, setSelectedInputOption] = useState<OptionTypeBase>(inputOptions[0]);
const [locations, setLocations] = useState(eventType.locations || []); const [locations, setLocations] = useState(eventType.locations || []);
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined); const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>( const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
@ -162,14 +163,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
}); });
const [hidden, setHidden] = useState<boolean>(eventType.hidden); const [hidden, setHidden] = useState<boolean>(eventType.hidden);
const titleRef = useRef<HTMLInputElement>(); const titleRef = useRef<HTMLInputElement>(null);
const slugRef = useRef<HTMLInputElement>(); const slugRef = useRef<HTMLInputElement>(null);
const descriptionRef = useRef<HTMLTextAreaElement>(); const descriptionRef = useRef<HTMLTextAreaElement>(null);
const lengthRef = useRef<HTMLInputElement>(); const lengthRef = useRef<HTMLInputElement>(null);
const requiresConfirmationRef = useRef<HTMLInputElement>(); const requiresConfirmationRef = useRef<HTMLInputElement>(null);
const eventNameRef = useRef<HTMLInputElement>(); const eventNameRef = useRef<HTMLInputElement>(null);
const periodDaysRef = useRef<HTMLInputElement>(); const periodDaysRef = useRef<HTMLInputElement>(null);
const periodDaysTypeRef = useRef<HTMLSelectElement>(); const periodDaysTypeRef = useRef<HTMLSelectElement>(null);
useEffect(() => { useEffect(() => {
setSelectedTimeZone(eventType.timeZone || user.timeZone); setSelectedTimeZone(eventType.timeZone || user.timeZone);
@ -804,17 +805,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</> </>
)} )}
</Disclosure> </Disclosure>
<div className="flex justify-end mt-4"> <div className="flex justify-end mt-4 space-x-2">
<Link href="/event-types"> <Button href="/event-types" color="secondary" tabIndex={-1}>
<a className="inline-flex items-center px-4 py-2 mr-2 text-sm font-medium bg-white border border-transparent rounded-sm shadow-sm text-neutral-700 hover:bg-neutral-100 border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Cancel Cancel
</a> </Button>
</Link> <Button type="submit">Update</Button>
<button
type="submit"
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Update
</button>
</div> </div>
</form> </form>
<Modal <Modal
@ -1033,7 +1028,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, query } = context; const { req, query } = context;
const session = await getSession({ req }); const session = await getSession({ req });
if (!session) { const typeParam = asStringOrThrow(query.type);
if (!session?.user?.id) {
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
@ -1042,22 +1039,38 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}; };
} }
const user: User = await prisma.user.findFirst({ const user = await prisma.user.findUnique({
where: { where: {
email: session.user.email, id: session.user.id,
}, },
select: { select: {
id: true,
username: true, username: true,
timeZone: true, timeZone: true,
startTime: true, startTime: true,
endTime: true, endTime: true,
availability: true, availability: true,
plan: true,
}, },
}); });
const eventType: EventType | null = await prisma.eventType.findUnique({ if (!user) {
return {
notFound: true,
} as const;
}
const eventType = await prisma.eventType.findFirst({
where: { where: {
id: parseInt(query.type as string), userId: user.id,
OR: [
{
slug: typeParam,
},
{
id: parseInt(typeParam),
},
],
}, },
select: { select: {
id: true, id: true,
@ -1116,10 +1129,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}, },
]; ];
const locationOptions: OptionBase[] = [ const locationOptions: OptionTypeBase[] = [
{ value: LocationType.InPerson, label: "In-person meeting" }, { value: LocationType.InPerson, label: "In-person meeting" },
{ value: LocationType.Phone, label: "Phone call" }, { value: LocationType.Phone, label: "Phone call" },
{ value: LocationType.Zoom, label: "Zoom Video" }, { value: LocationType.Zoom, label: "Zoom Video", disabled: true },
]; ];
const hasGoogleCalendarIntegration = integrations.find( const hasGoogleCalendarIntegration = integrations.find(

View file

@ -1,4 +1,4 @@
import { Dialog, DialogClose, DialogContent } from "@components/Dialog"; import { Dialog, DialogContent } from "@components/Dialog";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
import { Tooltip } from "@components/Tooltip"; import { Tooltip } from "@components/Tooltip";
import { Button } from "@components/ui/Button"; import { Button } from "@components/ui/Button";
@ -21,81 +21,57 @@ import { useRouter } from "next/router";
import React, { Fragment, useRef } from "react"; import React, { Fragment, useRef } from "react";
import Shell from "@components/Shell"; import Shell from "@components/Shell";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import { GetServerSidePropsContext } from "next";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import createEventType from "@lib/mutations/event-types/create-event-type"; import createEventType from "@lib/mutations/event-types/create-event-type";
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { Alert } from "@components/ui/Alert";
const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { const EventTypesPage = (props: inferSSRProps<typeof getServerSideProps>) => {
const { user, types } = props; const { user, types } = props;
const [session, loading] = useSession(); const [session, loading] = useSession();
const router = useRouter(); const router = useRouter();
const createMutation = useMutation(createEventType, { const createMutation = useMutation(createEventType, {
onSuccess: async ({ eventType }) => { onSuccess: async ({ eventType }) => {
await router.replace("/event-types/" + eventType.id); await router.push("/event-types/" + eventType.id);
showToast(`${eventType.title} event type created successfully`, "success"); showToast(`${eventType.title} event type created successfully`, "success");
}, },
onError: (err: Error) => { onError: (err: Error) => {
showToast(err.message, "error"); showToast(err.message, "error");
}, },
}); });
const modalOpen = useToggleQuery("new");
const titleRef = useRef<HTMLInputElement>(); const slugRef = useRef<HTMLInputElement>(null);
const slugRef = useRef<HTMLInputElement>();
const descriptionRef = useRef<HTMLTextAreaElement>();
const lengthRef = useRef<HTMLInputElement>();
const dialogOpen = router.query.new === "1";
async function createEventTypeHandler(event) {
event.preventDefault();
const enteredTitle = titleRef.current.value;
const enteredSlug = slugRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredLength = parseInt(lengthRef.current.value);
const body = {
title: enteredTitle,
slug: enteredSlug,
description: enteredDescription,
length: enteredLength,
};
createMutation.mutate(body);
}
function autoPopulateSlug() {
let t = titleRef.current.value;
t = t.replace(/\s+/g, "-").toLowerCase();
slugRef.current.value = t;
}
if (loading) { if (loading) {
return <Loader />; return <Loader />;
} }
const CreateNewEventDialog = () => ( const renderEventDialog = () => (
<Dialog <Dialog
open={dialogOpen} open={modalOpen.isOn}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
const newQuery = { router.push(isOpen ? modalOpen.hrefOn : modalOpen.hrefOff);
...router.query,
};
delete newQuery["new"];
if (!isOpen) {
router.push({ pathname: router.pathname, query: newQuery });
}
}}> }}>
<Button <Button
className="mt-2 hidden sm:block" className="mt-2 hidden sm:block"
StartIcon={PlusIcon} StartIcon={PlusIcon}
href={{ query: { ...router.query, new: "1" } }}> data-testid="new-event-type"
{...(props.canAddEvents
? {
href: modalOpen.hrefOn,
}
: {
disabled: true,
})}>
New event type New event type
</Button> </Button>
<Button size="fab" className="block sm:hidden" href={{ query: { ...router.query, new: "1" } }}> <Button size="fab" className="block sm:hidden" href={modalOpen.hrefOn}>
<PlusIcon className="w-8 h-8 text-white" /> <PlusIcon className="w-8 h-8 text-white" />
</Button> </Button>
@ -108,7 +84,24 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<p className="text-sm text-gray-500">Create a new event type for people to book times with.</p> <p className="text-sm text-gray-500">Create a new event type for people to book times with.</p>
</div> </div>
</div> </div>
<form onSubmit={createEventTypeHandler}> <form
onSubmit={(e) => {
e.preventDefault();
const target = e.target as unknown as Record<
"title" | "slug" | "description" | "length",
{ value: string }
>;
const body = {
title: target.title.value,
slug: target.slug.value,
description: target.description.value,
length: parseInt(target.length.value),
};
createMutation.mutate(body);
}}>
<div> <div>
<div className="mb-4"> <div className="mb-4">
<label htmlFor="title" className="block text-sm font-medium text-gray-700"> <label htmlFor="title" className="block text-sm font-medium text-gray-700">
@ -116,8 +109,13 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</label> </label>
<div className="mt-1"> <div className="mt-1">
<input <input
onChange={autoPopulateSlug} onChange={(e) => {
ref={titleRef} if (!slugRef.current) {
return;
}
const slug = e.target.value.replace(/\s+/g, "-").toLowerCase();
slugRef.current.value = slug;
}}
type="text" type="text"
name="title" name="title"
id="title" id="title"
@ -137,10 +135,10 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
{location.hostname}/{user.username}/ {location.hostname}/{user.username}/
</span> </span>
<input <input
ref={slugRef}
type="text" type="text"
name="slug" name="slug"
id="slug" id="slug"
ref={slugRef}
required required
className="flex-1 block w-full min-w-0 border-gray-300 rounded-none focus:border-neutral-900 rounded-r-md focus:ring-neutral-900 sm:text-sm" className="flex-1 block w-full min-w-0 border-gray-300 rounded-none focus:border-neutral-900 rounded-r-md focus:ring-neutral-900 sm:text-sm"
/> />
@ -153,7 +151,6 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</label> </label>
<div className="mt-1"> <div className="mt-1">
<textarea <textarea
ref={descriptionRef}
name="description" name="description"
id="description" id="description"
className="block w-full border-gray-300 rounded-sm shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm" className="block w-full border-gray-300 rounded-sm shadow-sm focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
@ -166,7 +163,6 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</label> </label>
<div className="relative mt-1 rounded-sm shadow-sm"> <div className="relative mt-1 rounded-sm shadow-sm">
<input <input
ref={lengthRef}
type="number" type="number"
name="length" name="length"
id="length" id="length"
@ -181,12 +177,12 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
</div> </div>
</div> </div>
<div className="mt-8 sm:flex sm:flex-row-reverse"> <div className="mt-8 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary"> <Button type="submit" loading={createMutation.isLoading}>
Continue Continue
</button> </Button>
<DialogClose as="button" className="mx-2 btn btn-white"> <Button href={modalOpen.hrefOff} color="secondary" className="mr-2">
Cancel Cancel
</DialogClose> </Button>
</div> </div>
</form> </form>
</DialogContent> </DialogContent>
@ -198,19 +194,41 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<Shell <Shell
heading="Event Types" heading="Event Types"
subtitle="Create events to share for people to book on your calendar." subtitle="Create events to share for people to book on your calendar."
CTA={types.length !== 0 && <CreateNewEventDialog />}> CTA={types.length !== 0 && renderEventDialog()}>
{props.user.plan === "FREE" && (
<Alert
severity="warning"
title={<>You need to upgrade your plan to have more than one active event type.</>}
message={
<>
To upgrade go to{" "}
<a href="https://calendso.com/upgrade" className="underline">
calendso.com/upgrade
</a>
</>
}
className="my-4"
/>
)}
<div className="-mx-4 overflow-hidden bg-white border border-gray-200 rounded-sm sm:mx-0"> <div className="-mx-4 overflow-hidden bg-white border border-gray-200 rounded-sm sm:mx-0">
<ul className="divide-y divide-neutral-200"> <ul className="divide-y divide-neutral-200" data-testid="event-types">
{types.map((type) => ( {types.map((item) => (
<li key={type.id}> <li
<div className="hover:bg-neutral-50"> key={item.id}
<div className="flex items-center px-4 py-4 sm:px-6"> className={classNames(
<Link href={"/event-types/" + type.id}> item.$disabled && "opacity-30 cursor-not-allowed pointer-events-none select-none"
<a className="flex-1 min-w-0 sm:flex sm:items-center sm:justify-between"> )}
data-disabled={item.$disabled ? 1 : 0}>
<div
className={classNames("hover:bg-neutral-50", item.$disabled && "pointer-events-none")}
tabIndex={item.$disabled ? -1 : undefined}>
<div className={"flex items-center px-4 py-4 sm:px-6"}>
<Link href={"/event-types/" + item.id}>
<a className="flex-1 min-w-0 sm:flex sm:items-center sm:justify-between hover:bg-neutral-50">
<span className="truncate"> <span className="truncate">
<div className="flex text-sm"> <div className="flex text-sm">
<p className="font-medium truncate text-neutral-900">{type.title}</p> <p className="font-medium truncate text-neutral-900">{item.title}</p>
{type.hidden && ( {item.hidden && (
<span className="inline-flex items-center ml-2 px-1.5 py-0.5 text-yellow-800 text-xs font-medium bg-yellow-100 rounded-sm"> <span className="inline-flex items-center ml-2 px-1.5 py-0.5 text-yellow-800 text-xs font-medium bg-yellow-100 rounded-sm">
Hidden Hidden
</span> </span>
@ -222,7 +240,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
className="flex-shrink-0 mr-1.5 w-4 h-4 text-neutral-400" className="flex-shrink-0 mr-1.5 w-4 h-4 text-neutral-400"
aria-hidden="true" aria-hidden="true"
/> />
<p>{type.length}m</p> <p>{item.length}m</p>
</div> </div>
<div className="flex items-center text-sm text-neutral-500"> <div className="flex items-center text-sm text-neutral-500">
<UserIcon <UserIcon
@ -231,14 +249,14 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
/> />
<p>1-on-1</p> <p>1-on-1</p>
</div> </div>
{type.description && ( {item.description && (
<div className="flex items-center text-sm text-neutral-500"> <div className="flex items-center text-sm text-neutral-500">
<InformationCircleIcon <InformationCircleIcon
className="flex-shrink-0 mr-1.5 w-4 h-4 text-neutral-400" className="flex-shrink-0 mr-1.5 w-4 h-4 text-neutral-400"
aria-hidden="true" aria-hidden="true"
/> />
<div className="truncate max-w-32 sm:max-w-full"> <div className="truncate max-w-32 sm:max-w-full">
{type.description.substring(0, 100)} {item.description.substring(0, 100)}
</div> </div>
</div> </div>
)} )}
@ -251,7 +269,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<div className="flex space-x-5 overflow-hidden"> <div className="flex space-x-5 overflow-hidden">
<Tooltip content="Preview"> <Tooltip content="Preview">
<a <a
href={"/" + session.user.username + "/" + type.slug} href={"/" + session.user.username + "/" + item.slug}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="p-2 border border-transparent cursor-pointer group text-neutral-400 hover:border-gray-200"> className="p-2 border border-transparent cursor-pointer group text-neutral-400 hover:border-gray-200">
@ -264,7 +282,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
onClick={() => { onClick={() => {
showToast("Link copied!", "success"); showToast("Link copied!", "success");
navigator.clipboard.writeText( navigator.clipboard.writeText(
window.location.hostname + "/" + session.user.username + "/" + type.slug window.location.hostname + "/" + session.user.username + "/" + item.slug
); );
}} }}
className="p-2 border border-transparent group text-neutral-400 hover:border-gray-200"> className="p-2 border border-transparent group text-neutral-400 hover:border-gray-200">
@ -300,7 +318,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<a <a
href={"/" + session.user.username + "/" + type.slug} href={"/" + session.user.username + "/" + item.slug}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={classNames( className={classNames(
@ -325,7 +343,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
"/" + "/" +
session.user.username + session.user.username +
"/" + "/" +
type.slug item.slug
); );
}} }}
className={classNames( className={classNames(
@ -633,7 +651,7 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
Event types enable you to share links that show available times on your calendar and allow Event types enable you to share links that show available times on your calendar and allow
people to make bookings with you. people to make bookings with you.
</p> </p>
<CreateNewEventDialog /> {renderEventDialog()}
</div> </div>
</div> </div>
)} )}
@ -646,13 +664,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { req } = context; const { req } = context;
const session = await getSession({ req }); const session = await getSession({ req });
if (!session) { if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } }; return {
redirect: {
permanent: false,
destination: "/auth/login",
},
} as const;
} }
const user = await prisma.user.findFirst({ const user = await prisma.user.findUnique({
where: { where: {
email: session.user.email, id: session.user.id,
}, },
select: { select: {
id: true, id: true,
@ -662,19 +685,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
bufferTime: true, bufferTime: true,
completedOnboarding: true, completedOnboarding: true,
createdDate: true, createdDate: true,
plan: true,
}, },
}); });
if (!user) {
// this shouldn't happen
return {
redirect: {
permanent: false,
destination: "/auth/login",
},
} as const;
}
if (!user.completedOnboarding && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT)) { if (!user.completedOnboarding && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT)) {
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/getting-started", destination: "/getting-started",
}, },
}; } as const;
} }
const types = await prisma.eventType.findMany({ const typesRaw = await prisma.eventType.findMany({
where: { where: {
userId: user.id, userId: user.id,
}, },
@ -688,14 +722,29 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}, },
}); });
const types = typesRaw.map((type, index) =>
user.plan === "FREE" && index > 0
? {
...type,
$disabled: true,
}
: {
...type,
$disabled: false,
}
);
const userObj = Object.assign({}, user, { const userObj = Object.assign({}, user, {
createdDate: user.createdDate.toString(), createdDate: user.createdDate.toString(),
}); });
const canAddEvents = user.plan !== "FREE" || types.length < 1;
return { return {
props: { props: {
user: userObj, user: userObj,
types, types,
canAddEvents,
}, },
}; };
}; };

54
pages/sandbox/Alert.tsx Normal file
View file

@ -0,0 +1,54 @@
import { Alert, AlertProps } from "@components/ui/Alert";
import Head from "next/head";
import React from "react";
export default function AlertPage() {
const list: AlertProps[] = [
{ title: "Something went wrong", severity: "error" },
{ title: "Something went kinda wrong", severity: "warning" },
{ title: "Something went great", severity: "success" },
{ title: "Something went wrong", severity: "error", message: "Some extra context" },
{
title: "Something went wrong",
severity: "error",
message: (
<p>
Some extra context
<br />
hey
</p>
),
},
];
return (
<>
<Head>
<meta name="googlebot" content="noindex" />
</Head>
<div className="p-4 bg-gray-200">
<h1>Alert component</h1>
<div className="flex flex-col">
{list.map((props, index) => (
<div key={index} className="p-2 m-2 bg-white">
<h3>
<code>
{JSON.stringify(
props,
(key, value) => {
if (key.includes("message")) {
return "..";
}
return value;
},
2
)}
</code>
</h3>
<Alert {...props}>Alert text</Alert>
</div>
))}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[userId,slug]` on the table `EventType` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "EventType.userId_slug_unique" ON "EventType"("userId", "slug");

View file

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "UserPlan" AS ENUM ('FREE', 'TRIAL', 'PRO');
-- AlterTable
ALTER TABLE "users" ADD COLUMN "plan" "UserPlan" NOT NULL DEFAULT E'PRO';

View file

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[username]` on the table `users` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "users.username_unique" ON "users"("username");

View file

@ -33,6 +33,8 @@ model EventType {
requiresConfirmation Boolean @default(false) requiresConfirmation Boolean @default(false)
minimumBookingNotice Int @default(120) minimumBookingNotice Int @default(120)
Schedule Schedule[] Schedule Schedule[]
@@unique([userId, slug])
} }
model Credential { model Credential {
@ -43,9 +45,15 @@ model Credential {
userId Int? userId Int?
} }
enum UserPlan {
FREE
TRIAL
PRO
}
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
username String? username String? @unique
name String? name String?
email String? @unique email String? @unique
emailVerified DateTime? emailVerified DateTime?
@ -68,6 +76,8 @@ model User {
selectedCalendars SelectedCalendar[] selectedCalendars SelectedCalendar[]
completedOnboarding Boolean? @default(false) completedOnboarding Boolean? @default(false)
plan UserPlan @default(PRO)
Schedule Schedule[] Schedule Schedule[]
@@map(name: "users") @@map(name: "users")
} }

View file

@ -2,7 +2,6 @@ import { hashPassword } from "../lib/auth";
import { Prisma, PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
let idx = 0;
async function createUserAndEventType(opts: { async function createUserAndEventType(opts: {
user: Omit<Prisma.UserCreateArgs["data"], "password" | "email"> & { password: string; email: string }; user: Omit<Prisma.UserCreateArgs["data"], "password" | "email"> & { password: string; email: string };
eventTypes: Array<Prisma.EventTypeCreateArgs["data"]>; eventTypes: Array<Prisma.EventTypeCreateArgs["data"]>;
@ -11,6 +10,7 @@ async function createUserAndEventType(opts: {
...opts.user, ...opts.user,
password: await hashPassword(opts.user.password), password: await hashPassword(opts.user.password),
emailVerified: new Date(), emailVerified: new Date(),
completedOnboarding: true,
}; };
const user = await prisma.user.upsert({ const user = await prisma.user.upsert({
where: { email: opts.user.email }, where: { email: opts.user.email },
@ -19,16 +19,17 @@ async function createUserAndEventType(opts: {
}); });
console.log( console.log(
`👤 Created '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 http://localhost:3000/${opts.user.username}` `👤 Upserted '${opts.user.username}' with email "${opts.user.email}" & password "${opts.user.password}". Booking page 👉 http://localhost:3000/${opts.user.username}`
); );
for (const rawData of opts.eventTypes) { for (const rawData of opts.eventTypes) {
const id = ++idx;
const eventTypeData: Prisma.EventTypeCreateArgs["data"] = { ...rawData }; const eventTypeData: Prisma.EventTypeCreateArgs["data"] = { ...rawData };
eventTypeData.userId = user.id; eventTypeData.userId = user.id;
eventTypeData.id = id;
await prisma.eventType.upsert({ await prisma.eventType.upsert({
where: { where: {
id, userId_slug: {
slug: eventTypeData.slug,
userId: user.id,
},
}, },
update: eventTypeData, update: eventTypeData,
create: eventTypeData, create: eventTypeData,
@ -45,7 +46,7 @@ async function main() {
email: "free@example.com", email: "free@example.com",
password: "free", password: "free",
username: "free", username: "free",
// plan: "FREE", plan: "FREE",
}, },
eventTypes: [ eventTypes: [
{ {
@ -60,12 +61,34 @@ async function main() {
}, },
], ],
}); });
await createUserAndEventType({
user: {
email: "free-first-hidden@example.com",
password: "free-first-hidden",
username: "free-first-hidden",
plan: "FREE",
},
eventTypes: [
{
title: "30min",
slug: "30min",
length: 30,
hidden: true,
},
{
title: "60min",
slug: "60min",
length: 30,
},
],
});
await createUserAndEventType({ await createUserAndEventType({
user: { user: {
email: "pro@example.com", email: "pro@example.com",
password: "pro", password: "pro",
username: "pro", username: "pro",
// plan: "PRO", plan: "PRO",
}, },
eventTypes: [ eventTypes: [
@ -86,7 +109,7 @@ async function main() {
email: "trial@example.com", email: "trial@example.com",
password: "trial", password: "trial",
username: "trial", username: "trial",
// plan: "TRIAL", plan: "TRIAL",
}, },
eventTypes: [ eventTypes: [
{ {

237
yarn.lock
View file

@ -11,6 +11,7 @@
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5":
version "7.14.5" version "7.14.5"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==
dependencies: dependencies:
"@babel/highlight" "^7.14.5" "@babel/highlight" "^7.14.5"
@ -359,7 +360,7 @@
"@emotion/weak-memoize" "^0.2.5" "@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1" hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^1.0.2": "@emotion/serialize@^1.0.0", "@emotion/serialize@^1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
dependencies: dependencies:
@ -1195,12 +1196,36 @@
"@types/react-outside-click-handler" "*" "@types/react-outside-click-handler" "*"
moment "^2.26.0" moment "^2.26.0"
"@types/react-dom@*":
version "17.0.9"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
dependencies:
"@types/react" "*"
"@types/react-outside-click-handler@*": "@types/react-outside-click-handler@*":
version "1.3.0" version "1.3.0"
resolved "https://registry.npmjs.org/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#ccf0014032fc6ec286210f8a05d26a5c1f94cc96" resolved "https://registry.npmjs.org/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#ccf0014032fc6ec286210f8a05d26a5c1f94cc96"
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-select@^4.0.17":
version "4.0.17"
resolved "https://registry.npmjs.org/@types/react-select/-/react-select-4.0.17.tgz#2e5ab4042c09c988bfc2711550329b0c3c9f8513"
integrity sha512-ZK5wcBhJaqC8ntQl0CJvK2KXNNsk1k5flM7jO+vNPPlceRzdJQazA6zTtQUyNr6exp5yrAiwiudtYxgGlgGHLg==
dependencies:
"@emotion/serialize" "^1.0.0"
"@types/react" "*"
"@types/react-dom" "*"
"@types/react-transition-group" "*"
"@types/react-transition-group@*":
version "4.4.2"
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz#38890fd9db68bf1f2252b99a942998dc7877c5b3"
integrity sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.18": "@types/react@*", "@types/react@^17.0.18":
version "17.0.18" version "17.0.18"
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.18.tgz#4109cbbd901be9582e5e39e3d77acd7b66bb7fbe" resolved "https://registry.npmjs.org/@types/react/-/react-17.0.18.tgz#4109cbbd901be9582e5e39e3d77acd7b66bb7fbe"
@ -1422,6 +1447,7 @@ airbnb-prop-types@^2.10.0, airbnb-prop-types@^2.14.0, airbnb-prop-types@^2.15.0:
ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
version "6.12.6" version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies: dependencies:
fast-deep-equal "^3.1.1" fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0" fast-json-stable-stringify "^2.0.0"
@ -1969,7 +1995,7 @@ caseless@~0.12.0:
resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chalk@2.4.2, chalk@^2.0.0: chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1:
version "2.4.2" version "2.4.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
dependencies: dependencies:
@ -2161,6 +2187,7 @@ colors@^1.1.2:
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8" version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
@ -2271,9 +2298,21 @@ cross-fetch@^3.1.4:
dependencies: dependencies:
node-fetch "2.6.1" node-fetch "2.6.1"
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies: dependencies:
path-key "^3.1.0" path-key "^3.1.0"
shebang-command "^2.0.0" shebang-command "^2.0.0"
@ -3428,6 +3467,11 @@ hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-
dependencies: dependencies:
react-is "^16.7.0" react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-encoding-sniffer@^2.0.1: html-encoding-sniffer@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
@ -3779,6 +3823,7 @@ is-typed-array@^1.1.3, is-typed-array@^1.1.6:
is-typedarray@^1.0.0, is-typedarray@~1.0.0: is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-unicode-supported@^0.1.0: is-unicode-supported@^0.1.0:
version "0.1.0" version "0.1.0"
@ -4295,6 +4340,11 @@ json-bigint@^1.0.0:
dependencies: dependencies:
bignumber.js "^9.0.0" bignumber.js "^9.0.0"
json-parse-better-errors@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
json-parse-even-better-errors@^2.3.0: json-parse-even-better-errors@^2.3.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
@ -4461,7 +4511,7 @@ lint-staged@^11.1.2:
string-argv "0.3.1" string-argv "0.3.1"
stringify-object "^3.3.0" stringify-object "^3.3.0"
listr2@^3.8.2, listr2@^3.8.3: listr2@^3.8.2:
version "3.11.0" version "3.11.0"
resolved "https://registry.npmjs.org/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9" resolved "https://registry.npmjs.org/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9"
dependencies: dependencies:
@ -4473,6 +4523,29 @@ listr2@^3.8.2, listr2@^3.8.3:
through "^2.3.8" through "^2.3.8"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
listr2@^3.8.3:
version "3.11.1"
resolved "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz#a9bab5cd5874fd3cb7827118dbea6fedefbcb43f"
integrity sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==
dependencies:
cli-truncate "^2.1.0"
colorette "^1.2.2"
log-update "^4.0.0"
p-map "^4.0.0"
rxjs "^6.6.7"
through "^2.3.8"
wrap-ansi "^7.0.0"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
dependencies:
graceful-fs "^4.1.2"
parse-json "^4.0.0"
pify "^3.0.0"
strip-bom "^3.0.0"
loader-utils@1.2.3: loader-utils@1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
@ -4554,6 +4627,7 @@ lodash@^4.1.1, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
log-symbols@^4.0.0, log-symbols@^4.1.0: log-symbols@^4.0.0, log-symbols@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
is-unicode-supported "^0.1.0" is-unicode-supported "^0.1.0"
@ -4615,6 +4689,11 @@ memoize-one@^5.0.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
merge-stream@^2.0.0: merge-stream@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -4830,6 +4909,11 @@ next@^11.1.1:
"@next/swc-linux-x64-gnu" "11.1.2" "@next/swc-linux-x64-gnu" "11.1.2"
"@next/swc-win32-x64-msvc" "11.1.2" "@next/swc-win32-x64-msvc" "11.1.2"
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-emoji@^1.8.1: node-emoji@^1.8.1:
version "1.11.0" version "1.11.0"
resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c"
@ -4894,6 +4978,16 @@ nodemailer@^6.4.16, nodemailer@^6.6.3:
version "6.6.3" version "6.6.3"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz#31fb53dd4d8ae16fc088a65cb9ffa8d928a69b48" resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz#31fb53dd4d8ae16fc088a65cb9ffa8d928a69b48"
normalize-package-data@^2.3.2:
version "2.5.0"
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
dependencies:
hosted-git-info "^2.1.4"
resolve "^1.10.0"
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
normalize-path@^3.0.0, normalize-path@~3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@ -4906,9 +5000,25 @@ normalize-wheel@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" resolved "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
npm-run-all@^4.1.5:
version "4.1.5"
resolved "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
dependencies:
ansi-styles "^3.2.1"
chalk "^2.4.1"
cross-spawn "^6.0.5"
memorystream "^0.3.1"
minimatch "^3.0.4"
pidtree "^0.3.0"
read-pkg "^3.0.0"
shell-quote "^1.6.1"
string.prototype.padend "^3.0.0"
npm-run-path@^4.0.0, npm-run-path@^4.0.1: npm-run-path@^4.0.0, npm-run-path@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies: dependencies:
path-key "^3.0.0" path-key "^3.0.0"
@ -5080,6 +5190,14 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5:
pbkdf2 "^3.0.3" pbkdf2 "^3.0.3"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
dependencies:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
parse-json@^5.0.0: parse-json@^5.0.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
@ -5119,6 +5237,11 @@ path-is-absolute@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-key@^3.0.0, path-key@^3.1.0: path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -5127,6 +5250,13 @@ path-parse@^1.0.6:
version "1.0.7" version "1.0.7"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
dependencies:
pify "^3.0.0"
path-type@^4.0.0: path-type@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@ -5154,11 +5284,21 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
pidtree@^0.3.0:
version "0.3.1"
resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==
pify@^2.2.0: pify@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
pirates@^4.0.1: pirates@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
@ -5344,6 +5484,7 @@ property-expr@^2.0.4:
psl@^1.1.28, psl@^1.1.33: psl@^1.1.28, psl@^1.1.33:
version "1.8.0" version "1.8.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
public-encrypt@^4.0.0: public-encrypt@^4.0.0:
version "4.0.3" version "4.0.3"
@ -5651,6 +5792,15 @@ react@17.0.2:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
dependencies:
load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6: readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6:
version "2.3.7" version "2.3.7"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@ -5757,7 +5907,7 @@ resolve-from@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
resolve@^1.20.0: resolve@^1.10.0, resolve@^1.20.0:
version "1.20.0" version "1.20.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
dependencies: dependencies:
@ -5840,7 +5990,7 @@ semver-compare@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
semver@^5.1.0, semver@^5.6.0: "semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0, semver@^5.6.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -5869,17 +6019,29 @@ sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
dependencies:
shebang-regex "^1.0.0"
shebang-command@^2.0.0: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
dependencies: dependencies:
shebang-regex "^3.0.0" shebang-regex "^3.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
shebang-regex@^3.0.0: shebang-regex@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
shell-quote@1.7.2: shell-quote@1.7.2, shell-quote@^1.6.1:
version "1.7.2" version "1.7.2"
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
@ -5971,6 +6133,32 @@ spacetime@^6.16.2:
version "6.16.3" version "6.16.3"
resolved "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz#86d3b05db33421a9ee478b1f2ca025582fc61fcf" resolved "https://registry.npmjs.org/spacetime/-/spacetime-6.16.3.tgz#86d3b05db33421a9ee478b1f2ca025582fc61fcf"
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
dependencies:
spdx-expression-parse "^3.0.0"
spdx-license-ids "^3.0.0"
spdx-exceptions@^2.1.0:
version "2.3.0"
resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
spdx-expression-parse@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
dependencies:
spdx-exceptions "^2.1.0"
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
version "3.0.10"
resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
sprintf-js@~1.0.2: sprintf-js@~1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -6081,6 +6269,15 @@ string.prototype.matchall@^4.0.5:
regexp.prototype.flags "^1.3.1" regexp.prototype.flags "^1.3.1"
side-channel "^1.0.4" side-channel "^1.0.4"
string.prototype.padend@^3.0.0:
version "3.1.2"
resolved "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.2.tgz#6858ca4f35c5268ebd5e8615e1327d55f59ee311"
integrity sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.2"
string.prototype.trimend@^1.0.4: string.prototype.trimend@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@ -6127,6 +6324,11 @@ strip-ansi@^3.0.0:
dependencies: dependencies:
ansi-regex "^2.0.0" ansi-regex "^2.0.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
strip-bom@^4.0.0: strip-bom@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
@ -6302,6 +6504,7 @@ timers-browserify@2.0.12, timers-browserify@^2.0.4:
tmp@^0.2.1, tmp@~0.2.1: tmp@^0.2.1, tmp@~0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
dependencies: dependencies:
rimraf "^3.0.0" rimraf "^3.0.0"
@ -6491,9 +6694,10 @@ typeorm@^0.2.30:
yargs "^17.0.1" yargs "^17.0.1"
zen-observable-ts "^1.0.0" zen-observable-ts "^1.0.0"
typescript@^4.3.5: typescript@^4.4.2:
version "4.3.5" version "4.4.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
uglify-js@^3.1.4: uglify-js@^3.1.4:
version "3.14.1" version "3.14.1"
@ -6609,6 +6813,14 @@ v8-to-istanbul@^8.0.0:
convert-source-map "^1.6.0" convert-source-map "^1.6.0"
source-map "^0.7.3" source-map "^0.7.3"
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
dependencies:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
verror@1.10.0: verror@1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
@ -6706,6 +6918,13 @@ which-typed-array@^1.1.2:
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
is-typed-array "^1.1.6" is-typed-array "^1.1.6"
which@^1.2.9:
version "1.3.1"
resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1: which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"