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 | ||||
| #   - 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
	
	 Shrey Gupta
						Shrey Gupta