feat(app-store): Add Giphy app (#2580)
This commit is contained in:
parent
276821e0b5
commit
21867c9cd4
20 changed files with 484 additions and 8 deletions
|
@ -14,6 +14,7 @@
|
||||||
# - STRIPE
|
# - STRIPE
|
||||||
# - TANDEM
|
# - TANDEM
|
||||||
# - ZOOM
|
# - ZOOM
|
||||||
|
# - GIPHY
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE *************************************************************************************************
|
||||||
# Set this value to 'agree' to accept our 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
|
# @see https://github.com/calcom/cal.com/#obtaining-zoom-client-id-and-secret
|
||||||
ZOOM_CLIENT_ID=
|
ZOOM_CLIENT_ID=
|
||||||
ZOOM_CLIENT_SECRET=
|
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 { JSONObject } from "superjson/dist/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SelectGifInput } from "@calcom/app-store/giphyother/components";
|
||||||
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
|
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
|
@ -199,7 +200,15 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
prefix: t("indefinitely_into_future"),
|
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();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -496,6 +505,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
externalId: string;
|
externalId: string;
|
||||||
};
|
};
|
||||||
successRedirectUrl: string;
|
successRedirectUrl: string;
|
||||||
|
giphyThankYouPage: string;
|
||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
locations: eventType.locations || [],
|
locations: eventType.locations || [],
|
||||||
|
@ -914,6 +924,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
periodDates,
|
periodDates,
|
||||||
periodCountCalendarDays,
|
periodCountCalendarDays,
|
||||||
smartContractAddress,
|
smartContractAddress,
|
||||||
|
giphyThankYouPage,
|
||||||
beforeBufferTime,
|
beforeBufferTime,
|
||||||
afterBufferTime,
|
afterBufferTime,
|
||||||
locations,
|
locations,
|
||||||
|
@ -931,11 +942,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
id: eventType.id,
|
id: eventType.id,
|
||||||
beforeEventBuffer: beforeBufferTime,
|
beforeEventBuffer: beforeBufferTime,
|
||||||
afterEventBuffer: afterBufferTime,
|
afterEventBuffer: afterBufferTime,
|
||||||
metadata: smartContractAddress
|
metadata: {
|
||||||
? {
|
...(smartContractAddress ? { smartContractAddress } : {}),
|
||||||
smartContractAddress,
|
...(giphyThankYouPage ? { giphyThankYouPage } : {}),
|
||||||
}
|
},
|
||||||
: "",
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="space-y-6">
|
className="space-y-6">
|
||||||
|
@ -1725,6 +1735,39 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
</div>
|
</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>
|
</CollapsibleContent>
|
||||||
</>
|
</>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
|
@ -2076,6 +2119,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
: false,
|
: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasGiphyIntegration = !!credentials.find((credential) => credential.type === "giphy_other");
|
||||||
|
|
||||||
// backwards compat
|
// backwards compat
|
||||||
if (eventType.users.length === 0 && !eventType.team) {
|
if (eventType.users.length === 0 && !eventType.team) {
|
||||||
const fallbackUser = await prisma.user.findUnique({
|
const fallbackUser = await prisma.user.findUnique({
|
||||||
|
@ -2133,6 +2178,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
team: eventTypeObject.team || null,
|
team: eventTypeObject.team || null,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
hasPaymentIntegration,
|
hasPaymentIntegration,
|
||||||
|
hasGiphyIntegration,
|
||||||
currency,
|
currency,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -159,6 +159,8 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
||||||
host: props.profile.name || "Nameless",
|
host: props.profile.name || "Nameless",
|
||||||
t,
|
t,
|
||||||
};
|
};
|
||||||
|
const metadata = props.eventType?.metadata as { giphyThankYouPage: string };
|
||||||
|
const giphyImage = metadata?.giphyThankYouPage;
|
||||||
|
|
||||||
const eventName = getEventName(eventNameObject);
|
const eventName = getEventName(eventNameObject);
|
||||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
|
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
|
||||||
|
@ -245,8 +247,13 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-headline">
|
aria-labelledby="modal-headline">
|
||||||
<div>
|
<div>
|
||||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
<div
|
||||||
{!needsConfirmation && <CheckIcon className="h-8 w-8 text-green-600" />}
|
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" />}
|
{needsConfirmation && <ClockIcon className="h-8 w-8 text-green-600" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
|
@ -468,6 +475,7 @@ const getEventTypesFromDB = async (typeId: number) => {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metadata: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -768,6 +768,8 @@
|
||||||
"edit_booking": "Edit booking",
|
"edit_booking": "Edit booking",
|
||||||
"reschedule_booking": "Reschedule booking",
|
"reschedule_booking": "Reschedule booking",
|
||||||
"former_time": "Former time",
|
"former_time": "Former time",
|
||||||
|
"confirmation_page_gif": "Gif for confirmation page",
|
||||||
|
"search": "Search",
|
||||||
"impersonate":"Impersonate",
|
"impersonate":"Impersonate",
|
||||||
"impersonate_user_tip":"All uses of this feature is audited.",
|
"impersonate_user_tip":"All uses of this feature is audited.",
|
||||||
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
|
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
|
||||||
|
|
|
@ -23,6 +23,7 @@ export const InstallAppButtonMap = {
|
||||||
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
||||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||||
|
giphyother: dynamic(() => import("./giphyother/components/InstallAppButton")),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const 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 applecalendar from "./applecalendar";
|
||||||
import * as caldavcalendar from "./caldavcalendar";
|
import * as caldavcalendar from "./caldavcalendar";
|
||||||
import * as dailyvideo from "./dailyvideo";
|
import * as dailyvideo from "./dailyvideo";
|
||||||
|
import * as giphyother from "./giphyother";
|
||||||
import * as googlecalendar from "./googlecalendar";
|
import * as googlecalendar from "./googlecalendar";
|
||||||
import * as googlevideo from "./googlevideo";
|
import * as googlevideo from "./googlevideo";
|
||||||
import * as hubspotothercalendar from "./hubspotothercalendar";
|
import * as hubspotothercalendar from "./hubspotothercalendar";
|
||||||
|
@ -32,6 +33,7 @@ const appStore = {
|
||||||
tandemvideo,
|
tandemvideo,
|
||||||
zoomvideo,
|
zoomvideo,
|
||||||
wipemycalother,
|
wipemycalother,
|
||||||
|
giphyother,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default appStore;
|
export default appStore;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
||||||
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
||||||
import { metadata as dailyvideo } from "./dailyvideo/_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 googlecalendar } from "./googlecalendar/_metadata";
|
||||||
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
||||||
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
||||||
|
@ -30,6 +31,7 @@ export const appStoreMetadata = {
|
||||||
tandemvideo,
|
tandemvideo,
|
||||||
zoomvideo,
|
zoomvideo,
|
||||||
wipemycalother,
|
wipemycalother,
|
||||||
|
giphy,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default appStoreMetadata;
|
export default appStoreMetadata;
|
||||||
|
|
Loading…
Reference in a new issue