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