move calendso branding into pro (#629)
* badge * mv branding to paid plan * upgrade ts * hideBranding check * user.plan * lint fixes * `isBrandingHidden` helper * hide pro for non-pros
This commit is contained in:
parent
8ee68e2ace
commit
ab78bb3802
8 changed files with 185 additions and 55 deletions
|
@ -1,8 +1,16 @@
|
|||
import { Fragment } from "react";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CheckIcon } from "@heroicons/react/outline";
|
||||
import { CheckIcon, InformationCircleIcon } from "@heroicons/react/outline";
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
export default function Modal(props) {
|
||||
export default function Modal(props: {
|
||||
heading: ReactNode;
|
||||
description: ReactNode;
|
||||
handleClose: () => void;
|
||||
open: boolean;
|
||||
variant?: "success" | "warning";
|
||||
}) {
|
||||
const { variant = "success" } = props;
|
||||
return (
|
||||
<Transition.Root show={props.open} as={Fragment}>
|
||||
<Dialog
|
||||
|
@ -37,8 +45,18 @@ export default function Modal(props) {
|
|||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||
<div>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<CheckIcon className="h-6 w-6 text-green-600" aria-hidden="true" />
|
||||
<div
|
||||
className={classNames(
|
||||
"mx-auto flex items-center justify-center h-12 w-12 rounded-full",
|
||||
variant === "success" && "bg-green-100",
|
||||
variant === "warning" && "bg-yellow-100"
|
||||
)}>
|
||||
{variant === "success" && (
|
||||
<CheckIcon className="h-6 w-6 text-green-600" aria-hidden="true" />
|
||||
)}
|
||||
{variant === "warning" && (
|
||||
<InformationCircleIcon className={"h-6 w-6 text-yellow-400"} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
|
||||
|
|
25
components/ui/Badge.tsx
Normal file
25
components/ui/Badge.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import classNames from "@lib/classNames";
|
||||
import React from "react";
|
||||
|
||||
export type BadgeProps = {
|
||||
variant: "default" | "success";
|
||||
} & JSX.IntrinsicElements["span"];
|
||||
|
||||
export const Badge = function Badge(props: BadgeProps) {
|
||||
const { variant, className, ...passThroughProps } = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
{...passThroughProps}
|
||||
className={classNames(
|
||||
"font-bold px-2 py-0.5 inline-block",
|
||||
variant === "default" && "bg-yellow-100 text-yellow-800",
|
||||
variant === "success" && "bg-green-100 text-green-800",
|
||||
className
|
||||
)}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
5
lib/isBrandingHidden.tsx
Normal file
5
lib/isBrandingHidden.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { User } from "@prisma/client";
|
||||
|
||||
export function isBrandingHidden<TUser extends Pick<User, "hideBranding" | "plan">>(user: TUser) {
|
||||
return user.hideBranding && user.plan !== "FREE";
|
||||
}
|
|
@ -93,7 +93,7 @@
|
|||
"prisma": "^2.30.2",
|
||||
"tailwindcss": "^2.2.7",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.4.2"
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./{*,{pages,components,lib}/**/*}.{js,ts,jsx,tsx}": [
|
||||
|
|
|
@ -9,6 +9,7 @@ import PoweredByCalendso from "@components/ui/PoweredByCalendso";
|
|||
import { ChevronDownIcon, ChevronUpIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid";
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
import prisma from "@lib/prisma";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
@ -179,7 +180,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!props.user.hideBranding && <PoweredByCalendso />}
|
||||
{!isBrandingHidden(props.user) && <PoweredByCalendso />}
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetServerSideProps } from "next";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { RefObject, useEffect, useRef, useState } from "react";
|
||||
import prisma from "@lib/prisma";
|
||||
import Modal from "@components/Modal";
|
||||
import Shell from "@components/Shell";
|
||||
|
@ -12,19 +12,80 @@ import { UsernameInput } from "@components/ui/UsernameInput";
|
|||
import ErrorAlert from "@components/ui/alerts/Error";
|
||||
import ImageUploader from "@components/ImageUploader";
|
||||
import crypto from "crypto";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import Badge from "@components/ui/Badge";
|
||||
import Button from "@components/ui/Button";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
|
||||
const themeOptions = [
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
];
|
||||
|
||||
export default function Settings(props) {
|
||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||
function HideBrandingInput(props: {
|
||||
//
|
||||
hideBrandingRef: RefObject<HTMLInputElement>;
|
||||
user: Props["user"];
|
||||
}) {
|
||||
const [modelOpen, setModalOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
id="hide-branding"
|
||||
name="hide-branding"
|
||||
type="checkbox"
|
||||
ref={props.hideBrandingRef}
|
||||
defaultChecked={isBrandingHidden(props.user)}
|
||||
className={
|
||||
"focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm disabled:opacity-50"
|
||||
}
|
||||
onClick={(e) => {
|
||||
if (!e.currentTarget.checked || props.user.plan !== "FREE") {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent checking the input
|
||||
e.preventDefault();
|
||||
|
||||
setModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
heading="This feature is only available in paid plan"
|
||||
variant="warning"
|
||||
description={
|
||||
<div className="flex flex-col space-y-3">
|
||||
<p>
|
||||
In order to remove the Calendso branding from your booking pages, you need to upgrade to a paid
|
||||
account.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{" "}
|
||||
To upgrade go to{" "}
|
||||
<a href="https://calendso.com/upgrade" className="underline">
|
||||
calendso.com/upgrade
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
open={modelOpen}
|
||||
handleClose={() => setModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings(props: Props) {
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
const usernameRef = useRef<HTMLInputElement>();
|
||||
const nameRef = useRef<HTMLInputElement>();
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const descriptionRef = useRef<HTMLTextAreaElement>();
|
||||
const avatarRef = useRef<HTMLInputElement>();
|
||||
const hideBrandingRef = useRef<HTMLInputElement>();
|
||||
const avatarRef = useRef<HTMLInputElement>(null);
|
||||
const hideBrandingRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme });
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
|
||||
const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart });
|
||||
|
@ -244,18 +305,12 @@ export default function Settings(props) {
|
|||
<div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="hide-branding"
|
||||
name="hide-branding"
|
||||
type="checkbox"
|
||||
ref={hideBrandingRef}
|
||||
defaultChecked={props.user.hideBranding}
|
||||
className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm"
|
||||
/>
|
||||
<HideBrandingInput user={props.user} hideBrandingRef={hideBrandingRef} />
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
||||
Disable Calendso branding
|
||||
Disable Calendso branding{" "}
|
||||
{props.user.plan !== "PRO" && <Badge variant="default">PRO</Badge>}
|
||||
</label>
|
||||
<p className="text-gray-500">Hide all Calendso branding from your public pages.</p>
|
||||
</div>
|
||||
|
@ -302,11 +357,7 @@ export default function Settings(props) {
|
|||
</div>
|
||||
<hr className="mt-8" />
|
||||
<div className="py-4 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500">
|
||||
Save
|
||||
</button>
|
||||
<Button type="submit">Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -321,9 +372,9 @@ export default function Settings(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getSession(context);
|
||||
if (!session) {
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
}
|
||||
|
||||
|
@ -342,9 +393,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
weekStart: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User seems logged in but cannot be found in the db");
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
user: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import Link from "next/link";
|
||||
import prisma, { whereAndSelect } from "@lib/prisma";
|
||||
import prisma from "@lib/prisma";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CheckIcon } from "@heroicons/react/outline";
|
||||
|
@ -12,12 +12,16 @@ import timezone from "dayjs/plugin/timezone";
|
|||
import { createEvent } from "ics";
|
||||
import { getEventName } from "@lib/event";
|
||||
import Theme from "@components/Theme";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { asStringOrNull } from "../lib/asStringOrNull";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(toArray);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export default function Success(props) {
|
||||
export default function Success(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const router = useRouter();
|
||||
const { location, name } = router.query;
|
||||
|
||||
|
@ -220,7 +224,7 @@ export default function Success(props) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!props.user.hideBranding && (
|
||||
{!isBrandingHidden(props.user) && (
|
||||
<div className="mt-4 pt-4 border-t dark:border-gray-900 text-gray-400 text-center text-xs dark:text-white">
|
||||
<a href="https://checkout.calendso.com">Create your own booking link with Calendso</a>
|
||||
</div>
|
||||
|
@ -235,30 +239,52 @@ export default function Success(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const user = context.query.user
|
||||
? await whereAndSelect(
|
||||
prisma.user.findFirst,
|
||||
{
|
||||
username: context.query.user,
|
||||
},
|
||||
["username", "name", "bio", "avatar", "hideBranding", "theme"]
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!user) {
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const username = asStringOrNull(context.query.user);
|
||||
const typeId = parseInt(asStringOrNull(context.query.type) ?? "");
|
||||
if (!username || isNaN(typeId)) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const eventType = await whereAndSelect(
|
||||
prisma.eventType.findUnique,
|
||||
{
|
||||
id: parseInt(context.query.type),
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
["id", "title", "description", "length", "eventName", "requiresConfirmation"]
|
||||
);
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: typeId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
length: true,
|
||||
eventName: true,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
});
|
||||
if (!eventType) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
|
|
@ -7667,10 +7667,10 @@ typeorm@^0.2.30:
|
|||
yargs "^17.0.1"
|
||||
zen-observable-ts "^1.0.0"
|
||||
|
||||
typescript@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
|
||||
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
|
||||
typescript@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
|
||||
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.14.2"
|
||||
|
|
Loading…
Reference in a new issue