diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index eeffac0457..aed4952683 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -845,6 +845,11 @@ "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions", "set_your_phone_number": "Set a phone number for the meeting", "calendar_no_busy_slots": "There are no busy slots", + "add_gif": "Add GIF", + "search_giphy": "Search Giphy", + "add_link_from_giphy": "Add link from Giphy", + "add_gif_to_confirmation": "Adding a GIF to confirmation page", + "find_gif_spice_confirmation": "Find GIF to spice up your confirmation page", "display_location_label":"Display on booking page", "display_location_info_badge":"Location will be visible before the booking is confirmed", "share_feedback": "Share feedback", diff --git a/packages/app-store/giphy/api/get.ts b/packages/app-store/giphy/api/get.ts new file mode 100644 index 0000000000..2afd608615 --- /dev/null +++ b/packages/app-store/giphy/api/get.ts @@ -0,0 +1,64 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { z, ZodError } from "zod"; + +import prisma from "@calcom/prisma"; + +import { GiphyManager } from "../lib"; + +const giphyUrlRegexp = new RegExp("^https://(.*).giphy.com/media/(.*)/giphy.gif(.*)"); + +const getSchema = z.object({ + url: z.string().regex(giphyUrlRegexp, "Giphy URL is invalid"), +}); + +/** + * 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 { url } = req.body; + const parsedUrl = new URL(url.replace(/ /g, "")); + // remove query strings if any that could cause trouble in parsing ID from url + const sanitisedUrl = parsedUrl.origin + parsedUrl.pathname; + // Extract Giphy ID from embed url + const matches = giphyUrlRegexp.exec(sanitisedUrl); + if (!matches || matches.length < 3) { + return res.status(400).json({ message: "Giphy URL is invalid" }); + } + const giphyId = matches[2]; + const gifImageUrl = await GiphyManager.getGiphyById(giphyId); + return res.status(200).json({ image: gifImageUrl }); + } catch (error: unknown) { + console.error({ error }); + if (error instanceof Error) { + return res.status(500).json({ message: error.message }); + } + return res.status(500); + } +} + +function validate(handler: (req: NextApiRequest, res: NextApiResponse) => Promise) { + return async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === "POST") { + try { + getSchema.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); diff --git a/packages/app-store/giphy/api/index.ts b/packages/app-store/giphy/api/index.ts index 8a66776473..c8140ff5bb 100644 --- a/packages/app-store/giphy/api/index.ts +++ b/packages/app-store/giphy/api/index.ts @@ -1,2 +1,3 @@ export { default as add } from "./add"; export { default as search } from "./search"; +export { default as get } from "./get"; diff --git a/packages/app-store/giphy/api/search.ts b/packages/app-store/giphy/api/search.ts index 58d3b9d82b..f255447bc0 100644 --- a/packages/app-store/giphy/api/search.ts +++ b/packages/app-store/giphy/api/search.ts @@ -32,8 +32,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }); 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 }); + const { gifImageUrl, total } = await GiphyManager.searchGiphy(locale, keyword, offset); + return res.status(200).json({ + image: gifImageUrl, + // rotate results to 0 offset when no more gifs + nextOffset: total === offset + 1 ? 0 : offset + 1, + }); } catch (error: unknown) { if (error instanceof Error) { return res.status(500).json({ message: error.message }); diff --git a/packages/app-store/giphy/components/SearchDialog.tsx b/packages/app-store/giphy/components/SearchDialog.tsx index 600556712e..3105ca7b1b 100644 --- a/packages/app-store/giphy/components/SearchDialog.tsx +++ b/packages/app-store/giphy/components/SearchDialog.tsx @@ -1,13 +1,12 @@ -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid"; +import { SearchIcon, LinkIcon } from "@heroicons/react/outline"; import { useState } from "react"; import { Dispatch, SetStateAction } from "react"; +import classNames from "@calcom/lib/classNames"; 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"; +import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/Dialog"; interface ISearchDialog { isOpenDialog: boolean; @@ -15,14 +14,19 @@ interface ISearchDialog { onSave: (url: string) => void; } +const MODE_SEARCH = "search" as const; +const MODE_URL = "url" as const; +type Mode = typeof MODE_SEARCH | typeof MODE_URL; + export const SearchDialog = (props: ISearchDialog) => { const { t } = useLocale(); const [gifImage, setGifImage] = useState(""); - const [offset, setOffset] = useState(0); + const [nextOffset, setNextOffset] = useState(0); const [keyword, setKeyword] = useState(""); const { isOpenDialog, setIsOpenDialog } = props; const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(""); + const [selectedMode, setSelectedMode] = useState(MODE_SEARCH); const searchGiphy = async (keyword: string, offset: number) => { if (isLoading) { @@ -45,7 +49,7 @@ export const SearchDialog = (props: ISearchDialog) => { setErrorMessage(json?.message || "Something went wrong"); } else { setGifImage(json.image || ""); - setOffset(offset); + setNextOffset(json.nextOffset); if (!json.image) { setErrorMessage("No Result found"); } @@ -54,72 +58,134 @@ export const SearchDialog = (props: ISearchDialog) => { return null; }; + const getGiphyByUrl = async (url: string) => { + if (isLoading) { + return; + } + setIsLoading(true); + setErrorMessage(""); + const res = await fetch("/api/integrations/giphy/get", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url, + }), + }); + const json = await res.json(); + if (!res.ok) { + setErrorMessage(json?.message || json?.[0]?.message || "Something went wrong"); + } else { + setGifImage(json.image || ""); + if (!json.image) { + setErrorMessage("No Result found"); + } + } + setIsLoading(false); + return null; + }; + + const renderTab = (Icon: any, text: string, mode: Mode) => ( +
{ + setKeyword(""); + setGifImage(""); + setSelectedMode(mode); + }}> + + {text} +
+ ); + + const handleFormSubmit = async (event: React.SyntheticEvent) => { + event.stopPropagation(); + event.preventDefault(); + if (selectedMode === MODE_SEARCH) { + searchGiphy(keyword, 0); + } else if (selectedMode === MODE_URL) { + getGiphyByUrl(keyword); + } + }; + return ( - - -
- { - setKeyword(event.target.value); - }} - name="search" - type="text" - className="mt-2" - labelProps={{ style: { display: "none" } }} - placeholder="Search Giphy" - /> - -
+ {gifImage && (
- {isLoading ? ( - - ) : ( - <> -
- {`Gif +
+ {isLoading ? ( +
+ + + +
-
- -
- - )} + ) : ( + {`Gif + )} +
)} {errorMessage && } + {gifImage && selectedMode === MODE_SEARCH && ( +
+
Not the perfect GIF?
+ +
+ )} { @@ -133,16 +199,16 @@ export const SearchDialog = (props: ISearchDialog) => { diff --git a/packages/app-store/giphy/components/SelectGifInput.tsx b/packages/app-store/giphy/components/SelectGifInput.tsx index ffb3de11b3..739aa1f00e 100644 --- a/packages/app-store/giphy/components/SelectGifInput.tsx +++ b/packages/app-store/giphy/components/SelectGifInput.tsx @@ -1,4 +1,4 @@ -import { SearchIcon, TrashIcon } from "@heroicons/react/solid"; +import { PlusIcon, PencilAltIcon, XIcon } from "@heroicons/react/solid"; import { useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -18,19 +18,26 @@ export default function SelectGifInput(props: ISelectGifInput) { return (
{selectedGif && ( -
+
{"Selected
)}
- + {selectedGif ? ( + + ) : ( + + )} + {selectedGif && (