diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 224a74d6c3..ccfe1f8427 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -10,21 +10,21 @@ import { GetServerSidePropsContext } from "next"; import { useSession } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; +import { SpaceBookingSuccessPage } from "@calcom/app-store/spacebooking/components"; import { - useIsEmbed, - useEmbedStyles, - useIsBackgroundTransparent, + sdkActionManager, useEmbedNonStylesConfig, + useIsBackgroundTransparent, + useIsEmbed, } from "@calcom/embed-core"; -import { sdkActionManager } from "@calcom/embed-core"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/Button"; import { EmailInput } from "@calcom/ui/form/fields"; -import { asStringOrThrow, asStringOrNull } from "@lib/asStringOrNull"; +import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import { getEventName } from "@lib/event"; import useTheme from "@lib/hooks/useTheme"; import { isBrandingHidden } from "@lib/isBrandingHidden"; @@ -215,228 +215,258 @@ export default function Success(props: inferSSRProps) const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id))); return ( (isReady && ( -
- - - -
-
- {isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? ( - - ) : null}{" "} -
+ <> +
+ + + +
+
+ {isSuccessRedirectAvailable(eventType) && eventType.successRedirectUrl ? ( + + ) : null}{" "} -
-
+
+
+ {/* SPACE BOOKING APP */} + {props.userHasSpaceBooking && ( + + )} + )) || null ); @@ -499,6 +529,21 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { notFound: true, }; } + let spaceBookingAvailable = false; + + let userHasSpaceBooking = false; + if (eventType.users[0] && eventType.users[0].id) { + const credential = await prisma.credential.findFirst({ + where: { + type: "spacebooking_other", + userId: eventType.users[0].id, + }, + }); + if (credential && credential.type === "spacebooking_other") { + userHasSpaceBooking = true; + } + } + if (!eventType.users.length && eventType.userId) { // TODO we should add `user User` relation on `EventType` so this extra query isn't needed const user = await prisma.user.findUnique({ @@ -545,6 +590,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { eventType, trpcState: ssr.dehydrate(), dynamicEventName, + userHasSpaceBooking, }, }; } diff --git a/packages/app-store/apiHandlers.tsx b/packages/app-store/apiHandlers.tsx index c6b988041f..b1c6068729 100644 --- a/packages/app-store/apiHandlers.tsx +++ b/packages/app-store/apiHandlers.tsx @@ -15,6 +15,7 @@ export const apiHandlers = { huddle01video: import("./huddle01video/api"), metamask: import("./metamask/api"), giphy: import("./giphy/api"), + spacebookingother: import("./spacebooking/api"), // @todo Until we use DB slugs everywhere zapierother: import("./zapier/api"), }; diff --git a/packages/app-store/components.tsx b/packages/app-store/components.tsx index c36b7a08be..2ca1929f79 100644 --- a/packages/app-store/components.tsx +++ b/packages/app-store/components.tsx @@ -26,6 +26,7 @@ export const InstallAppButtonMap = { huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")), metamask: dynamic(() => import("./metamask/components/InstallAppButton")), giphy: dynamic(() => import("./giphy/components/InstallAppButton")), + spacebookingother: dynamic(() => import("./spacebooking/components/InstallAppButton")), }; export const InstallAppButton = ( diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index c7232b556f..468086c645 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -12,6 +12,7 @@ import * as metamask from "./metamask"; import * as office365calendar from "./office365calendar"; import * as office365video from "./office365video"; import * as slackmessaging from "./slackmessaging"; +import * as spacebooking from "./spacebooking"; import * as stripepayment from "./stripepayment"; import * as tandemvideo from "./tandemvideo"; import * as wipemycalother from "./wipemycalother"; @@ -32,6 +33,7 @@ const appStore = { office365video, slackmessaging, stripepayment, + spacebooking, tandemvideo, zoomvideo, wipemycalother, diff --git a/packages/app-store/metadata.ts b/packages/app-store/metadata.ts index 5fb28a6414..d8b8ed37e3 100644 --- a/packages/app-store/metadata.ts +++ b/packages/app-store/metadata.ts @@ -11,6 +11,7 @@ import { metadata as metamask } from "./metamask/_metadata"; import { metadata as office365calendar } from "./office365calendar/_metadata"; import { metadata as office365video } from "./office365video/_metadata"; import { metadata as slackmessaging } from "./slackmessaging/_metadata"; +import { metadata as spacebooking } from "./spacebooking/_metadata"; import { metadata as stripepayment } from "./stripepayment/_metadata"; import { metadata as tandemvideo } from "./tandemvideo/_metadata"; import { metadata as wipemycalother } from "./wipemycalother/_metadata"; @@ -30,6 +31,7 @@ export const appStoreMetadata = { office365video, slackmessaging, stripepayment, + spacebooking, tandemvideo, zoomvideo, wipemycalother, diff --git a/packages/app-store/spacebooking/README.mdx b/packages/app-store/spacebooking/README.mdx new file mode 100644 index 0000000000..d829970254 --- /dev/null +++ b/packages/app-store/spacebooking/README.mdx @@ -0,0 +1,10 @@ + + +Looking to honor May 4th? Search no further. Download this app to make your booking success page resemble a long time ago in a galaxy far far away. diff --git a/packages/app-store/spacebooking/_metadata.ts b/packages/app-store/spacebooking/_metadata.ts new file mode 100644 index 0000000000..5199ad95ae --- /dev/null +++ b/packages/app-store/spacebooking/_metadata.ts @@ -0,0 +1,26 @@ +import type { App } from "@calcom/types/App"; + +import _package from "./package.json"; + +export const metadata = { + name: "SpaceBooking", + description: _package.description, + installed: true, + category: "other", + // If using static next public folder, can then be referenced from the base URL (/). + imageSrc: "/api/app-store/spacebooking/spacebooking.webp", + logo: "/api/app-store/spacebooking/spacebooking.webp", + publisher: "Cal.com", + rating: 0, + reviews: 0, + slug: "space-booking", + title: "SpaceBooking", + trending: true, + type: "spacebooking_other", + url: "https://cal.com/apps/spacebooking", + variant: "other", + verified: true, + email: "help@cal.com", +} as App; + +export default metadata; diff --git a/packages/app-store/spacebooking/api/add.ts b/packages/app-store/spacebooking/api/add.ts new file mode 100644 index 0000000000..d185f6248b --- /dev/null +++ b/packages/app-store/spacebooking/api/add.ts @@ -0,0 +1,43 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +/** + * @param req + * @param res + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session?.user?.id) { + return res.status(401).json({ message: "You must be logged in to do this" }); + } + const appType = "spacebooking_other"; + try { + const alreadyInstalled = await prisma.credential.findFirst({ + where: { + type: appType, + userId: req.session.user.id, + }, + }); + if (alreadyInstalled) { + throw new Error("Already installed"); + } + const installation = await prisma.credential.create({ + data: { + type: appType, + key: {}, + userId: req.session.user.id, + appId: "space-booking", + }, + }); + if (!installation) { + throw new Error("Unable to create user credential for spacebooking"); + } + } catch (error: unknown) { + if (error instanceof Error) { + return res.status(500).json({ message: error.message }); + } + return res.status(500); + } + + return res.status(200).json({ url: "/apps/installed" }); +} diff --git a/packages/app-store/spacebooking/api/index.ts b/packages/app-store/spacebooking/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/spacebooking/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/spacebooking/components/FullScreenDialog.tsx b/packages/app-store/spacebooking/components/FullScreenDialog.tsx new file mode 100644 index 0000000000..1229abe75b --- /dev/null +++ b/packages/app-store/spacebooking/components/FullScreenDialog.tsx @@ -0,0 +1,30 @@ +import { XIcon } from "@heroicons/react/solid"; +import { useState } from "react"; + +import { Dialog, DialogContent } from "@calcom/ui/Dialog"; + +const FullScreenDialog = (props: React.PropsWithChildren<{ open: boolean }>) => { + const [open, setOpen] = useState(props.open); + return ( + <> + setOpen(false)}> + +
setOpen(false)} + style={{ + position: "absolute", + top: 16, + right: 16, + zIndex: "inherit", + }}> + +
+ {props.children} +
+
+ + ); +}; + +export { FullScreenDialog }; diff --git a/packages/app-store/spacebooking/components/InstallAppButton.tsx b/packages/app-store/spacebooking/components/InstallAppButton.tsx new file mode 100644 index 0000000000..bb535357bd --- /dev/null +++ b/packages/app-store/spacebooking/components/InstallAppButton.tsx @@ -0,0 +1,17 @@ +import useAddAppMutation from "../../_utils/useAddAppMutation"; +import { InstallAppButtonProps } from "../../types"; + +export default function InstallAppButton(props: InstallAppButtonProps) { + const mutation = useAddAppMutation("spacebooking_other"); + + return ( + <> + {props.render({ + onClick() { + mutation.mutate(""); + }, + loading: mutation.isLoading, + })} + + ); +} diff --git a/packages/app-store/spacebooking/components/SpaceBookingSuccessPage.tsx b/packages/app-store/spacebooking/components/SpaceBookingSuccessPage.tsx new file mode 100644 index 0000000000..5fc8b77b92 --- /dev/null +++ b/packages/app-store/spacebooking/components/SpaceBookingSuccessPage.tsx @@ -0,0 +1,230 @@ +import { useEffect } from "react"; + +import { FullScreenDialog } from "./FullScreenDialog"; + +const SpaceBookingSuccessPage = (props: { what: string; where: string; when: string; open: boolean }) => { + const { what, where, when, open } = props; + + useEffect(() => { + const canvas = document.getElementById("canvas") as HTMLCanvasElement; + const c = canvas.getContext("2d") as CanvasRenderingContext2D; + + let w: number; + let h: number; + + const setCanvasExtents = () => { + w = document.body.clientWidth; + h = document.body.clientHeight; + canvas.width = Math.min(1600, w); + canvas.height = Math.min(900, h); + }; + + setCanvasExtents(); + + const crawl = document.getElementById("crawl") as HTMLDivElement; + const crawlContent = document.getElementById("crawl-content") as HTMLDivElement; + const crawlContentStyle = crawlContent.style; + + // start crawl at bottom of 3d plane + let crawlPos = crawl.clientHeight; + + const makeStars = (count: number) => { + const out = []; + for (let i = 0; i < count; i++) { + const s = { + x: Math.random() * 1600 - 800, + y: Math.random() * 900 - 450, + z: Math.random() * 1000, + }; + out.push(s); + } + return out; + }; + + let stars = makeStars(2000); + + window.onresize = () => { + setCanvasExtents(); + }; + + const clear = () => { + c.fillStyle = "black"; + c.fillRect(0, 0, canvas.width, canvas.height); + }; + + const putPixel = (x: number, y: number, brightness: number) => { + const intensity = brightness * 255; + const rgb = "rgb(" + intensity + "," + intensity + "," + intensity + ")"; + c.fillStyle = rgb; + c.fillRect(x, y, 1, 1); + }; + + const moveStars = (distance: number) => { + const count = stars.length; + for (var i = 0; i < count; i++) { + const s = stars[i]; + s.z -= distance; + if (s.z <= 1) { + s.z += 999; + } + } + }; + + const moveCrawl = (distance: number) => { + crawlPos -= distance; + crawlContentStyle.top = crawlPos + "px"; + + // if we've scrolled all content past the top edge + // of the plane, reposition content at bottom of plane + if (crawlPos < -crawlContent.clientHeight) { + crawlPos = crawl.clientHeight; + } + }; + + const paintStars = () => { + const cx = canvas.width / 2; + const cy = canvas.height / 2; + + const count = stars.length; + for (var i = 0; i < count; i++) { + const star = stars[i]; + + const x = cx + star.x / (star.z * 0.001); + const y = cy + star.y / (star.z * 0.001); + + if (x < 0 || x >= w || y < 0 || y >= h) { + continue; + } + + const d = star.z / 1000.0; + const b = 1 - d * d; + + putPixel(x, y, b); + } + }; + + let prevTime: number; + const init = (time: number) => { + prevTime = time; + requestAnimationFrame(tick); + }; + + const tick = (time: number) => { + let elapsed = time - prevTime; + prevTime = time; + + moveStars(elapsed * 0.02); + + // time-scale of crawl, increase factor to go faster + moveCrawl(elapsed * 0.06); + + clear(); + paintStars(); + + requestAnimationFrame(tick); + }; + + requestAnimationFrame(init); + }, []); + + const episodeDetails = [ + { number: "IV", name: "A new hope" }, + { number: "V", name: "The Empire" }, + { number: "VI", name: "Return of the Jedi" }, + { number: "I", name: "The Phantom menace" }, + { number: "II", name: "Attack of the Clones" }, + { number: "III", name: "Revenge of the Sith" }, + ]; + const randomEpisodeNumber = episodeDetails[Math.floor(Math.random() * episodeDetails.length)]; + + const css = ` + #crawl-container { + perspective: calc(100vh * 0.4); + } + #crawl { + color: #f5c91c; + position: absolute; + width: 110%; + left: -5%; + bottom: -5%; + height: 200%; + overflow: hidden; + + transform: rotate3d(1, 0, 0, 45deg); + transform-origin: 50% 100%; + + mask-image: linear-gradient( + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0.66), + rgba(0, 0, 0, 1) + ); + + -webkit-mask-image: -webkit-linear-gradient( + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0.66), + rgba(0, 0, 0, 1) + ); + } + + #crawl-content { + font-family: "Roboto"; + font-size: calc(100vw * 0.074); + letter-spacing: 0.09em; + position: absolute; + top: 0px; + left: 0px; + right: 0px; + } + + #crawl p { + text-align: justify; + width: 100%; + margin: 0 0 1.25em 0; + line-height: 1.25em; + } + + #crawl h1 { + font-size: 1em; + margin: 0 0 0.3em 0; + } + + #crawl h2 { + font-size: 1.5em; + margin: 0 0 0.7em 0; + } + + #crawl h1, + #crawl h2 { + text-align: center; + } +`; + + return ( + <> + + +
+
+
+

Episode {randomEpisodeNumber.number}

+

{randomEpisodeNumber.name}

+

{what}

+

+ {when} +
+

+

{where}

+
+
+
+ +
+ + ); +}; + +export default SpaceBookingSuccessPage; diff --git a/packages/app-store/spacebooking/components/index.ts b/packages/app-store/spacebooking/components/index.ts new file mode 100644 index 0000000000..40e82ee54e --- /dev/null +++ b/packages/app-store/spacebooking/components/index.ts @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic"; + +export { default as InstallAppButton } from "./InstallAppButton"; +/** These are like 12kb that not every user needs */ +export const SpaceBookingSuccessPage = dynamic(() => import("./SpaceBookingSuccessPage")); diff --git a/packages/app-store/spacebooking/index.ts b/packages/app-store/spacebooking/index.ts new file mode 100644 index 0000000000..19f36d184d --- /dev/null +++ b/packages/app-store/spacebooking/index.ts @@ -0,0 +1,4 @@ +export * as api from "./api"; +export * as lib from "./lib"; +export { metadata } from "./_metadata"; +export * as components from "./components"; diff --git a/packages/app-store/spacebooking/lib/index.ts b/packages/app-store/spacebooking/lib/index.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/app-store/spacebooking/lib/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/app-store/spacebooking/package.json b/packages/app-store/spacebooking/package.json new file mode 100644 index 0000000000..c299e2d79b --- /dev/null +++ b/packages/app-store/spacebooking/package.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/spacebooking", + "version": "0.1.0", + "main": "./index.ts", + "description": "Looking to honor May 4th? Search no further. Install this app to make your booking success page resemble a long time ago in a galaxy far far away.", + "dependencies": {}, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/spacebooking/static/spacebooking.webp b/packages/app-store/spacebooking/static/spacebooking.webp new file mode 100644 index 0000000000..3da82b8310 Binary files /dev/null and b/packages/app-store/spacebooking/static/spacebooking.webp differ diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index 8942b96440..18d29cb54f 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -9,6 +9,7 @@ async function createApp( /** The directory name for `/packages/app-store/[dirName]` */ dirName: Prisma.AppCreateInput["dirName"], categories: Prisma.AppCreateInput["categories"], + /** This is used so credentials gets linked to the correct app */ type: Prisma.CredentialCreateInput["type"], keys?: Prisma.AppCreateInput["keys"] ) { @@ -84,6 +85,7 @@ async function main() { api_key: process.env.GIPHY_API_KEY, }); } + await createApp("space-booking", "spacebooking", ["other"], "spacebooking_other"); await createApp("zapier", "zapier", ["other"], "zapier_other"); // Web3 apps await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video"); diff --git a/packages/ui/Dialog.tsx b/packages/ui/Dialog.tsx index 80d8fa3c89..9bd4f882ad 100644 --- a/packages/ui/Dialog.tsx +++ b/packages/ui/Dialog.tsx @@ -2,6 +2,8 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"; import { useRouter } from "next/router"; import React, { ReactNode, useState } from "react"; +import classNames from "@calcom/lib/classNames"; + export type DialogProps = React.ComponentProps & { name?: string; clearQueryParamsOnClose?: string[]; @@ -63,7 +65,10 @@ export const DialogContent = React.forwardRef {children}