feat(app-store): Add Giphy app (#2580)
This commit is contained in:
parent
df801b4205
commit
11f6972ec9
20 changed files with 484 additions and 8 deletions
|
@ -14,6 +14,7 @@
|
|||
# - STRIPE
|
||||
# - TANDEM
|
||||
# - ZOOM
|
||||
# - GIPHY
|
||||
|
||||
# - LICENSE *************************************************************************************************
|
||||
# Set this value to 'agree' to accept our license:
|
||||
|
@ -168,4 +169,9 @@ TANDEM_BASE_URL="https://tandem.chat"
|
|||
# @see https://github.com/calcom/cal.com/#obtaining-zoom-client-id-and-secret
|
||||
ZOOM_CLIENT_ID=
|
||||
ZOOM_CLIENT_SECRET=
|
||||
|
||||
# - GIPHY
|
||||
# Used for the Giphy integration
|
||||
# @see https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key
|
||||
GIPHY_API_KEY=
|
||||
# *********************************************************************************************************
|
||||
|
|
|
@ -29,6 +29,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
|||
import { JSONObject } from "superjson/dist/types";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SelectGifInput } from "@calcom/app-store/giphyother/components";
|
||||
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
|
@ -199,7 +200,15 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
prefix: t("indefinitely_into_future"),
|
||||
},
|
||||
];
|
||||
const { eventType, locationOptions, team, teamMembers, hasPaymentIntegration, currency } = props;
|
||||
const {
|
||||
eventType,
|
||||
locationOptions,
|
||||
team,
|
||||
teamMembers,
|
||||
hasPaymentIntegration,
|
||||
currency,
|
||||
hasGiphyIntegration,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -496,6 +505,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
externalId: string;
|
||||
};
|
||||
successRedirectUrl: string;
|
||||
giphyThankYouPage: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
locations: eventType.locations || [],
|
||||
|
@ -914,6 +924,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
smartContractAddress,
|
||||
giphyThankYouPage,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
locations,
|
||||
|
@ -931,11 +942,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
id: eventType.id,
|
||||
beforeEventBuffer: beforeBufferTime,
|
||||
afterEventBuffer: afterBufferTime,
|
||||
metadata: smartContractAddress
|
||||
? {
|
||||
smartContractAddress,
|
||||
}
|
||||
: "",
|
||||
metadata: {
|
||||
...(smartContractAddress ? { smartContractAddress } : {}),
|
||||
...(giphyThankYouPage ? { giphyThankYouPage } : {}),
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="space-y-6">
|
||||
|
@ -1725,6 +1735,39 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasGiphyIntegration && (
|
||||
<>
|
||||
<hr className="border-neutral-200" />
|
||||
<div className="block sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="gif"
|
||||
className="mt-2 flex text-sm font-medium text-neutral-700">
|
||||
{t("confirmation_page_gif")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full">
|
||||
<div className="block items-center sm:flex">
|
||||
<div className="w-full">
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center">
|
||||
<SelectGifInput
|
||||
defaultValue={eventType?.metadata?.giphyThankYouPage as string}
|
||||
onChange={(url) => {
|
||||
formMethods.setValue("giphyThankYouPage", url);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</>
|
||||
{/* )} */}
|
||||
|
@ -2076,6 +2119,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
: false,
|
||||
};
|
||||
|
||||
const hasGiphyIntegration = !!credentials.find((credential) => credential.type === "giphy_other");
|
||||
|
||||
// backwards compat
|
||||
if (eventType.users.length === 0 && !eventType.team) {
|
||||
const fallbackUser = await prisma.user.findUnique({
|
||||
|
@ -2133,6 +2178,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
team: eventTypeObject.team || null,
|
||||
teamMembers,
|
||||
hasPaymentIntegration,
|
||||
hasGiphyIntegration,
|
||||
currency,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -159,6 +159,8 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
host: props.profile.name || "Nameless",
|
||||
t,
|
||||
};
|
||||
const metadata = props.eventType?.metadata as { giphyThankYouPage: string };
|
||||
const giphyImage = metadata?.giphyThankYouPage;
|
||||
|
||||
const eventName = getEventName(eventNameObject);
|
||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
|
||||
|
@ -245,8 +247,13 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
{!needsConfirmation && <CheckIcon className="h-8 w-8 text-green-600" />}
|
||||
<div
|
||||
className={classNames(
|
||||
"mx-auto flex items-center justify-center",
|
||||
!giphyImage ? "h-12 w-12 rounded-full bg-green-100" : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && <img src={giphyImage} alt={"Gif from Giphy"} />}
|
||||
{!giphyImage && !needsConfirmation && <CheckIcon className="h-8 w-8 text-green-600" />}
|
||||
{needsConfirmation && <ClockIcon className="h-8 w-8 text-green-600" />}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
|
@ -468,6 +475,7 @@ const getEventTypesFromDB = async (typeId: number) => {
|
|||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -768,6 +768,8 @@
|
|||
"edit_booking": "Edit booking",
|
||||
"reschedule_booking": "Reschedule booking",
|
||||
"former_time": "Former time",
|
||||
"confirmation_page_gif": "Gif for confirmation page",
|
||||
"search": "Search",
|
||||
"impersonate":"Impersonate",
|
||||
"impersonate_user_tip":"All uses of this feature is audited.",
|
||||
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
|
||||
|
|
|
@ -23,6 +23,7 @@ export const InstallAppButtonMap = {
|
|||
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||
giphyother: dynamic(() => import("./giphyother/components/InstallAppButton")),
|
||||
};
|
||||
|
||||
export const InstallAppButton = (
|
||||
|
|
26
packages/app-store/giphyother/_metadata.ts
Normal file
26
packages/app-store/giphyother/_metadata.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
export const metadata = {
|
||||
name: "Giphy",
|
||||
description: _package.description,
|
||||
installed: !!process.env.GIPHY_API_KEY,
|
||||
category: "other",
|
||||
// If using static next public folder, can then be referenced from the base URL (/).
|
||||
imageSrc: "/api/app-store/giphyother/icon.svg",
|
||||
logo: "/api/app-store/giphyother/icon.svg",
|
||||
publisher: "Cal.com",
|
||||
rating: 0,
|
||||
reviews: 0,
|
||||
slug: "giphy",
|
||||
title: "Giphy",
|
||||
trending: true,
|
||||
type: "giphy_other",
|
||||
url: "https://cal.com/apps/giphy",
|
||||
variant: "other",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as App;
|
||||
|
||||
export default metadata;
|
43
packages/app-store/giphyother/api/add.ts
Normal file
43
packages/app-store/giphyother/api/add.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
/**
|
||||
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
|
||||
* @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 = "giphy_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,
|
||||
},
|
||||
});
|
||||
if (!installation) {
|
||||
throw new Error("Unable to create user credential for giphy");
|
||||
}
|
||||
} 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" });
|
||||
}
|
2
packages/app-store/giphyother/api/index.ts
Normal file
2
packages/app-store/giphyother/api/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as add } from "./add";
|
||||
export { default as search } from "./search";
|
63
packages/app-store/giphyother/api/search.ts
Normal file
63
packages/app-store/giphyother/api/search.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { GiphyManager } from "../lib";
|
||||
|
||||
const searchSchema = z.object({
|
||||
keyword: z.string(),
|
||||
offset: z.number().min(0),
|
||||
});
|
||||
|
||||
/**
|
||||
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const userId = req.session?.user?.id;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
locale: true,
|
||||
},
|
||||
});
|
||||
const locale = user?.locale || "en";
|
||||
const { keyword, offset } = req.body;
|
||||
const gifImageUrl = await GiphyManager.searchGiphy(locale, keyword, offset);
|
||||
return res.status(200).json({ image: gifImageUrl });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
return res.status(500);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(handler: (req: NextApiRequest, res: NextApiResponse) => Promise<NextApiResponse | void>) {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "POST") {
|
||||
try {
|
||||
searchSchema.parse(req.body);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError && error?.name === "ZodError") {
|
||||
return res.status(400).json(error?.issues);
|
||||
}
|
||||
return res.status(402);
|
||||
}
|
||||
} else {
|
||||
return res.status(405);
|
||||
}
|
||||
await handler(req, res);
|
||||
};
|
||||
}
|
||||
|
||||
export default validate(handler);
|
|
@ -0,0 +1,17 @@
|
|||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
import { InstallAppButtonProps } from "../../types";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("giphy_other");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
151
packages/app-store/giphyother/components/SearchDialog.tsx
Normal file
151
packages/app-store/giphyother/components/SearchDialog.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
||||
import { useState } from "react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { TextField } from "@calcom/ui/form/fields";
|
||||
import Loader from "@calcom/web/components/Loader";
|
||||
|
||||
interface ISearchDialog {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
onSave: (url: string) => void;
|
||||
}
|
||||
|
||||
export const SearchDialog = (props: ISearchDialog) => {
|
||||
const { t } = useLocale();
|
||||
const [gifImage, setGifImage] = useState<string>("");
|
||||
const [offset, setOffset] = useState<number>(0);
|
||||
const [keyword, setKeyword] = useState<string>("");
|
||||
const { isOpenDialog, setIsOpenDialog } = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const searchGiphy = async (keyword: string, offset: number) => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/giphyother/search", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
keyword,
|
||||
offset,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrorMessage(json?.message || "Something went wrong");
|
||||
} else {
|
||||
setGifImage(json.image || "");
|
||||
setOffset(offset);
|
||||
if (!json.image) {
|
||||
setErrorMessage("No Result found");
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader title="Search a gif" />
|
||||
|
||||
<div className="flex justify-center space-x-2 space-y-2">
|
||||
<TextField
|
||||
value={keyword}
|
||||
onChange={(event) => {
|
||||
setKeyword(event.target.value);
|
||||
}}
|
||||
name="search"
|
||||
type="text"
|
||||
className="mt-2"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
placeholder="Search Giphy"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={(event) => {
|
||||
searchGiphy(keyword, 0);
|
||||
return false;
|
||||
}}
|
||||
loading={isLoading}>
|
||||
{t("search")}
|
||||
</Button>
|
||||
</div>
|
||||
{gifImage && (
|
||||
<div className="flex flex-col items-center space-x-2 space-y-2 pt-3">
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<img src={gifImage} alt={`Gif from Giphy for ${keyword}`} />
|
||||
</div>
|
||||
<div>
|
||||
<nav>
|
||||
<ul className="inline-flex space-x-2">
|
||||
<li style={{ visibility: offset <= 0 ? "hidden" : "visible" }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
searchGiphy(keyword, offset - 1);
|
||||
}}
|
||||
className="focus:shadow-outline flex h-10 w-10 items-center justify-center rounded-full text-indigo-600 transition-colors duration-150 hover:bg-indigo-100">
|
||||
<ChevronLeftIcon />
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
onClick={() => {
|
||||
searchGiphy(keyword, offset + 1);
|
||||
}}
|
||||
className="focus:shadow-outline flex h-10 w-10 items-center justify-center rounded-full bg-white text-indigo-600 transition-colors duration-150 hover:bg-indigo-100">
|
||||
<ChevronRightIcon />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<DialogFooter>
|
||||
<DialogClose
|
||||
onClick={() => {
|
||||
props.setIsOpenDialog(false);
|
||||
}}
|
||||
asChild>
|
||||
<Button type="button" color="minimal" tabIndex={-1}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
loading={isLoading}
|
||||
onClick={() => {
|
||||
props.setIsOpenDialog(false);
|
||||
props.onSave(gifImage);
|
||||
setOffset(0);
|
||||
setGifImage("");
|
||||
setKeyword("");
|
||||
return false;
|
||||
}}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
52
packages/app-store/giphyother/components/SelectGifInput.tsx
Normal file
52
packages/app-store/giphyother/components/SelectGifInput.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { SearchIcon, TrashIcon } from "@heroicons/react/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import { SearchDialog } from "./SearchDialog";
|
||||
|
||||
interface ISelectGifInput {
|
||||
defaultValue?: string | null;
|
||||
onChange: (url: string) => void;
|
||||
}
|
||||
export default function SelectGifInput(props: ISelectGifInput) {
|
||||
const { t } = useLocale();
|
||||
const [selectedGif, setSelectedGif] = useState(props.defaultValue);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start space-x-2 space-y-2">
|
||||
{selectedGif && (
|
||||
<div>
|
||||
<img alt={"Selected Gif Image"} src={selectedGif} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<Button color="secondary" type="button" StartIcon={SearchIcon} onClick={() => setShowDialog(true)}>
|
||||
Search on Giphy
|
||||
</Button>
|
||||
{selectedGif && (
|
||||
<Button
|
||||
color="warn"
|
||||
type="button"
|
||||
StartIcon={TrashIcon}
|
||||
onClick={() => {
|
||||
setSelectedGif("");
|
||||
props.onChange("");
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<SearchDialog
|
||||
isOpenDialog={showDialog}
|
||||
setIsOpenDialog={setShowDialog}
|
||||
onSave={(url) => {
|
||||
setSelectedGif(url);
|
||||
props.onChange(url);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
2
packages/app-store/giphyother/components/index.ts
Normal file
2
packages/app-store/giphyother/components/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
export { default as SelectGifInput } from "./SelectGifInput";
|
4
packages/app-store/giphyother/index.ts
Normal file
4
packages/app-store/giphyother/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
export { metadata } from "./_metadata";
|
||||
export * as components from "./components";
|
20
packages/app-store/giphyother/lib/giphyManager.ts
Normal file
20
packages/app-store/giphyother/lib/giphyManager.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export const searchGiphy = async (locale: string, keyword: string, offset: number = 0) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
api_key: String(process.env.GIPHY_API_KEY),
|
||||
q: keyword,
|
||||
limit: "1",
|
||||
offset: String(offset),
|
||||
// Contains images that are broadly accepted as appropriate and commonly witnessed by people in a public environment.
|
||||
rating: "g",
|
||||
lang: locale,
|
||||
});
|
||||
const response = await fetch(`https://api.giphy.com/v1/gifs/search?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const responseBody = await response.json();
|
||||
const gifs = responseBody.data;
|
||||
return gifs?.[0]?.images?.fixed_height_downsampled?.url || null;
|
||||
};
|
1
packages/app-store/giphyother/lib/index.ts
Normal file
1
packages/app-store/giphyother/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as GiphyManager from "./giphyManager";
|
14
packages/app-store/giphyother/package.json
Normal file
14
packages/app-store/giphyother/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/giphy",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "GIPHY is your top source for the best & newest GIFs & Animated Stickers online. Find everything from funny GIFs, reaction GIFs, unique GIFs and more.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
}
|
||||
}
|
14
packages/app-store/giphyother/static/icon.svg
Normal file
14
packages/app-store/giphyother/static/icon.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<svg height="2500" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="4 2 16.32 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M6.331 4.286H17.99v15.428H6.33z" fill="#000" />
|
||||
<g fill-rule="nonzero">
|
||||
<path d="M4 3.714h2.331v16.572H4z" fill="#04ff8e" />
|
||||
<path d="M17.989 8.286h2.331v12h-2.331z" fill="#8e2eff" />
|
||||
<path d="M4 19.714h16.32V22H4z" fill="#00c5ff" />
|
||||
<path d="M4 2h9.326v2.286H4z" fill="#fff152" />
|
||||
<path d="M17.989 6.571V4.286h-2.332V2h-2.331v6.857h6.994V6.571" fill="#ff5b5b" />
|
||||
<path d="M17.989 11.143V8.857h2.331" fill="#551c99" />
|
||||
</g>
|
||||
<path d="M13.326 2v2.286h-2.332" fill="#999131" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 734 B |
|
@ -2,6 +2,7 @@
|
|||
import * as applecalendar from "./applecalendar";
|
||||
import * as caldavcalendar from "./caldavcalendar";
|
||||
import * as dailyvideo from "./dailyvideo";
|
||||
import * as giphyother from "./giphyother";
|
||||
import * as googlecalendar from "./googlecalendar";
|
||||
import * as googlevideo from "./googlevideo";
|
||||
import * as hubspotothercalendar from "./hubspotothercalendar";
|
||||
|
@ -32,6 +33,7 @@ const appStore = {
|
|||
tandemvideo,
|
||||
zoomvideo,
|
||||
wipemycalother,
|
||||
giphyother,
|
||||
};
|
||||
|
||||
export default appStore;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
||||
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
||||
import { metadata as dailyvideo } from "./dailyvideo/_metadata";
|
||||
import { metadata as giphy } from "./giphyother/_metadata";
|
||||
import { metadata as googlecalendar } from "./googlecalendar/_metadata";
|
||||
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
||||
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
||||
|
@ -30,6 +31,7 @@ export const appStoreMetadata = {
|
|||
tandemvideo,
|
||||
zoomvideo,
|
||||
wipemycalother,
|
||||
giphy,
|
||||
};
|
||||
|
||||
export default appStoreMetadata;
|
||||
|
|
Loading…
Reference in a new issue