2
0

Implement Pexels videos option to media popover (#1636)

Closes #1575 

Note: Need to create a new environment variable named
`NEXT_PUBLIC_PEXELS_API_KEY` to store the API Key obtained from Pexels!


https://github.com/user-attachments/assets/4250f799-0bd7-48e9-b9a8-4bc188ad7704

---------

Co-authored-by: Baptiste Arnaud <baptiste.arnaud95@gmail.com>
Co-authored-by: younesbenallal <younes.benallal.06@gmail.com>
This commit is contained in:
Abhirup Basu
2024-07-22 23:11:01 +05:30
committed by GitHub
parent 94ca8ac39f
commit 09277c264c
10 changed files with 507 additions and 98 deletions

View File

@ -82,6 +82,7 @@
"nprogress": "0.2.0",
"openai": "4.47.1",
"papaparse": "5.4.1",
"pexels": "^1.4.0",
"prettier": "2.8.8",
"qs": "6.11.2",
"react": "18.2.0",
@ -113,6 +114,7 @@
"@typebot.io/schemas": "workspace:*",
"@typebot.io/telemetry": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
"@types/canvas-confetti": "1.6.0",
"@types/jsonwebtoken": "9.0.2",
"@types/micro-cors": "0.1.3",
@ -131,7 +133,6 @@
"next-runtime-env": "1.6.2",
"superjson": "1.12.4",
"typescript": "5.4.5",
"zod": "3.22.4",
"@typebot.io/variables": "workspace:*"
"zod": "3.22.4"
}
}

View File

@ -31,6 +31,7 @@ export const GiphyPicker = ({ onSubmit }: GiphySearchFormProps) => {
placeholder="Search..."
onChange={setInputValue}
withVariableButton={false}
width="full"
/>
<GiphyLogo w="100px" />
</Flex>

View File

@ -0,0 +1,254 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
Alert,
AlertIcon,
Box,
Flex,
Grid,
GridItem,
HStack,
Image,
Link,
Spinner,
Stack,
Text,
} from '@chakra-ui/react'
import { isDefined } from '@typebot.io/lib'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createClient, Video, ErrorResponse, Videos } from 'pexels'
import { TextInput } from '../inputs'
import { TextLink } from '../TextLink'
import { env } from '@typebot.io/env'
import { PexelsLogo } from '../logos/PexelsLogo'
const client = createClient(env.NEXT_PUBLIC_PEXELS_API_KEY ?? 'dummy')
type Props = {
videoSize: 'large' | 'medium' | 'small'
onVideoSelect: (videoUrl: string) => void
}
export const PexelsPicker = ({ videoSize, onVideoSelect }: Props) => {
const [isFetching, setIsFetching] = useState(false)
const [videos, setVideos] = useState<Video[]>([])
const [error, setError] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const scrollContainer = useRef<HTMLDivElement>(null)
const bottomAnchor = useRef<HTMLDivElement>(null)
const [nextPage, setNextPage] = useState(0)
const fetchNewVideos = useCallback(async (query: string, page: number) => {
if (query === '') getInitialVideos()
if (query.length <= 2) {
setNextPage(0)
return
}
setError(null)
setIsFetching(true)
try {
const result = await client.videos.search({
query,
per_page: 24,
size: videoSize,
page,
})
if ((result as ErrorResponse).error)
setError((result as ErrorResponse).error)
if (isDefined((result as Videos).videos)) {
if (page === 0) setVideos((result as Videos).videos)
else
setVideos((videos) => [
...videos,
...((result as Videos)?.videos ?? []),
])
setNextPage((page) => page + 1)
}
} catch (err) {
if (err && typeof err === 'object' && 'message' in err)
setError(err.message as string)
setError('Something went wrong')
}
setIsFetching(false)
}, [])
useEffect(() => {
if (!bottomAnchor.current) return
const observer = new IntersectionObserver(
(entities: IntersectionObserverEntry[]) => {
const target = entities[0]
if (target.isIntersecting) fetchNewVideos(searchQuery, nextPage + 1)
},
{
root: scrollContainer.current,
}
)
if (bottomAnchor.current && nextPage > 0)
observer.observe(bottomAnchor.current)
return () => {
observer.disconnect()
}
}, [fetchNewVideos, nextPage, searchQuery])
const getInitialVideos = async () => {
setError(null)
setIsFetching(true)
client.videos
.popular({
per_page: 24,
size: videoSize,
})
.then((res) => {
if ((res as ErrorResponse).error) {
setError((res as ErrorResponse).error)
}
setVideos((res as Videos).videos)
setIsFetching(false)
})
.catch((err) => {
if (err && typeof err === 'object' && 'message' in err)
setError(err.message as string)
setError('Something went wrong')
setIsFetching(false)
})
}
const selectVideo = (video: Video) => {
const videoUrl = video.video_files[0].link
if (isDefined(videoUrl)) onVideoSelect(videoUrl)
}
useEffect(() => {
if (!env.NEXT_PUBLIC_PEXELS_API_KEY) return
getInitialVideos()
}, [])
if (!env.NEXT_PUBLIC_PEXELS_API_KEY)
return <Text>NEXT_PUBLIC_PEXELS_API_KEY is missing in environment</Text>
return (
<Stack spacing={4} pt="2">
<HStack align="center">
<TextInput
autoFocus
placeholder="Search..."
onChange={(query) => {
setSearchQuery(query)
fetchNewVideos(query, 0)
}}
withVariableButton={false}
debounceTimeout={500}
forceDebounce
width="full"
/>
<Link isExternal href={`https://www.pexels.com`}>
<PexelsLogo width="100px" height="40px" />
</Link>
</HStack>
{isDefined(error) && (
<Alert status="error">
<AlertIcon />
{error}
</Alert>
)}
<Stack overflowY="auto" maxH="400px" ref={scrollContainer}>
{videos.length > 0 && (
<Grid templateColumns="repeat(3, 1fr)" columnGap={2} rowGap={3}>
{videos.map((video, index) => (
<GridItem
as={Stack}
key={video.id}
boxSize="100%"
spacing="0"
ref={index === videos.length - 1 ? bottomAnchor : undefined}
>
<PexelsVideo video={video} onClick={() => selectVideo(video)} />
</GridItem>
))}
</Grid>
)}
{isFetching && (
<Flex justifyContent="center" py="4">
<Spinner />
</Flex>
)}
</Stack>
</Stack>
)
}
type PexelsVideoProps = {
video: Video
onClick: () => void
}
const PexelsVideo = ({ video, onClick }: PexelsVideoProps) => {
const { user, url, video_pictures } = video
const [isImageHovered, setIsImageHovered] = useState(false)
const [thumbnailImage, setThumbnailImage] = useState(
video_pictures[0].picture
)
const [imageIndex, setImageIndex] = useState(1)
useEffect(() => {
let interval: NodeJS.Timer
if (isImageHovered && video_pictures.length > 0) {
interval = setInterval(() => {
setImageIndex((prevIndex) => (prevIndex + 1) % video_pictures.length)
setThumbnailImage(video_pictures[imageIndex].picture)
}, 200)
} else {
setThumbnailImage(video_pictures[0].picture)
setImageIndex(1)
}
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [isImageHovered, imageIndex, video_pictures])
return (
<Box
pos="relative"
onMouseEnter={() => setIsImageHovered(true)}
onMouseLeave={() => setIsImageHovered(false)}
h="full"
>
{
<Image
objectFit="cover"
src={thumbnailImage}
alt={`Pexels Video ${video.id}`}
onClick={onClick}
rounded="md"
h="100%"
aspectRatio={4 / 3}
cursor="pointer"
/>
}
<Box
pos="absolute"
bottom={0}
left={0}
bgColor="rgba(0,0,0,.5)"
px="2"
rounded="md"
opacity={isImageHovered ? 1 : 0}
transition="opacity .2s ease-in-out"
>
<TextLink
fontSize="xs"
isExternal
href={url}
noOfLines={1}
color="white"
>
{user.name}
</TextLink>
</Box>
</Box>
)
}

View File

@ -0,0 +1,110 @@
import { Stack, Text } from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { VideoBubbleBlock } from '@typebot.io/schemas'
import { TextInput } from '@/components/inputs'
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
import { SwitchWithLabel } from '../inputs/SwitchWithLabel'
export const VideoLinkEmbedContent = ({
content,
updateUrl,
onSubmit,
}: {
content?: VideoBubbleBlock['content']
updateUrl: (url: string) => void
onSubmit: (content: VideoBubbleBlock['content']) => void
}) => {
const { t } = useTranslate()
const updateAspectRatio = (aspectRatio?: string) => {
return onSubmit({
...content,
aspectRatio,
})
}
const updateMaxWidth = (maxWidth?: string) => {
return onSubmit({
...content,
maxWidth,
})
}
const updateAutoPlay = (isAutoplayEnabled: boolean) => {
return onSubmit({ ...content, isAutoplayEnabled })
}
const updateControlsDisplay = (areControlsDisplayed: boolean) => {
if (areControlsDisplayed === false) {
// Make sure autoplay is enabled when video controls are disabled
return onSubmit({
...content,
isAutoplayEnabled: true,
areControlsDisplayed,
})
}
return onSubmit({ ...content, areControlsDisplayed })
}
return (
<>
<Stack py="2">
<TextInput
placeholder={t('video.urlInput.placeholder')}
defaultValue={content?.url ?? ''}
onChange={updateUrl}
/>
<Text fontSize="xs" color="gray.400" textAlign="center">
{t('video.urlInput.helperText')}
</Text>
</Stack>
{content?.url && (
<Stack>
<TextInput
label={t('video.aspectRatioInput.label')}
moreInfoTooltip={t('video.aspectRatioInput.moreInfoTooltip')}
defaultValue={
content?.aspectRatio ?? defaultVideoBubbleContent.aspectRatio
}
onChange={updateAspectRatio}
direction="row"
/>
<TextInput
label={t('video.maxWidthInput.label')}
moreInfoTooltip={t('video.maxWidthInput.moreInfoTooltip')}
defaultValue={
content?.maxWidth ?? defaultVideoBubbleContent.maxWidth
}
onChange={updateMaxWidth}
direction="row"
/>
</Stack>
)}
{content?.url && content?.type === 'url' && (
<Stack>
<SwitchWithLabel
label={'Display controls'}
initialValue={
content?.areControlsDisplayed ??
defaultVideoBubbleContent.areControlsDisplayed
}
onCheckChange={updateControlsDisplay}
/>
<SwitchWithLabel
label={t('editor.blocks.bubbles.audio.settings.autoplay.label')}
initialValue={
content?.isAutoplayEnabled ??
defaultVideoBubbleContent.isAutoplayEnabled
}
isChecked={
content?.isAutoplayEnabled ??
defaultVideoBubbleContent.isAutoplayEnabled
}
isDisabled={content?.areControlsDisplayed === false}
onCheckChange={() => updateAutoPlay(!content.isAutoplayEnabled)}
/>
</Stack>
)}
</>
)
}

View File

@ -1,4 +1,4 @@
import { IconProps, Icon } from '@chakra-ui/react'
import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
export const GiphyLogo = (props: IconProps) => (
<Icon viewBox="0 0 163.79999999999998 35" {...props}>
@ -15,7 +15,7 @@ export const GiphyLogo = (props: IconProps) => (
<path d="M16 0v4h-4" fill="#999131" />
<path
d="M59.1 12c-2-1.9-4.4-2.4-6.2-2.4-4.4 0-7.3 2.6-7.3 8 0 3.5 1.8 7.8 7.3 7.8 1.4 0 3.7-.3 5.2-1.4v-3.5h-6.9v-6h13.3v12.1c-1.7 3.5-6.4 5.3-11.7 5.3-10.7 0-14.8-7.2-14.8-14.3S42.7 3.2 52.9 3.2c3.8 0 7.1.8 10.7 4.4zm9.1 19.2V4h7.6v27.2zm20.1-7.4v7.3h-7.7V4h13.2c7.3 0 10.9 4.6 10.9 9.9 0 5.6-3.6 9.9-10.9 9.9zm0-6.5h5.5c2.1 0 3.2-1.6 3.2-3.3 0-1.8-1.1-3.4-3.2-3.4h-5.5zM125 31.2V20.9h-9.8v10.3h-7.7V4h7.7v10.3h9.8V4h7.6v27.2zm24.2-17.9l5.9-9.3h8.7v.3l-10.8 16v10.8h-7.7V20.3L135 4.3V4h8.7z"
fill="#000"
fill={useColorModeValue('#000', '#fff')}
fillRule="nonzero"
/>
</g>

View File

@ -0,0 +1,25 @@
import { IconProps, Icon, useColorModeValue } from '@chakra-ui/react'
export const PexelsLogo = (props: IconProps) => (
<Icon viewBox="0 0 130.318 50" {...props}>
<g transform="translate(-3894 2762)">
<rect
width="50"
height="50"
rx="8"
transform="translate(3894 -2762)"
fill="#07a081"
/>
<path
d="M32.671,44.73h7.091V37.935H41.9a5.657,5.657,0,1,0,0-11.314H32.671Zm10.763,3.622H29V23H41.9a9.271,9.271,0,0,1,1.53,18.435Z"
transform="translate(3880 -2773)"
fill="#fff"
/>
<path
d="M1.694,0h2.6V-6.16H7.656a6.579,6.579,0,0,0,2.915-.616,4.639,4.639,0,0,0,1.969-1.76,5.1,5.1,0,0,0,.7-2.728,5.146,5.146,0,0,0-.7-2.75,4.639,4.639,0,0,0-1.969-1.76,6.579,6.579,0,0,0-2.915-.616H1.694Zm2.6-8.47v-5.61H7.722a3.03,3.03,0,0,1,2.134.748,2.641,2.641,0,0,1,.814,2.046A2.684,2.684,0,0,1,9.856-9.24a2.978,2.978,0,0,1-2.134.77ZM20.372.264a5.925,5.925,0,0,0,3.179-.836,4.64,4.64,0,0,0,1.9-2.112l-2.024-.99a3.73,3.73,0,0,1-1.2,1.243,3.29,3.29,0,0,1-1.837.5A3.458,3.458,0,0,1,18-2.827a3.433,3.433,0,0,1-1.1-2.409H25.74a3.34,3.34,0,0,0,.088-.572q.022-.308.022-.594a6.154,6.154,0,0,0-.671-2.849,5.361,5.361,0,0,0-1.936-2.112,5.61,5.61,0,0,0-3.069-.8,5.7,5.7,0,0,0-3,.8,5.773,5.773,0,0,0-2.1,2.2,6.476,6.476,0,0,0-.77,3.179A6.482,6.482,0,0,0,15.081-2.8,5.9,5.9,0,0,0,17.226-.561,5.958,5.958,0,0,0,20.372.264Zm-.2-10.34a3,3,0,0,1,2.112.792,2.9,2.9,0,0,1,.924,2.068H16.94a3.313,3.313,0,0,1,1.122-2.112A3.208,3.208,0,0,1,20.174-10.076ZM26.422,0h2.926l2.706-3.982L34.738,0h2.926L33.506-5.962l4.18-5.94H34.76L32.054-7.964,29.348-11.9H26.422l4.158,5.94ZM44.088.264a5.925,5.925,0,0,0,3.179-.836,4.64,4.64,0,0,0,1.9-2.112l-2.024-.99a3.73,3.73,0,0,1-1.2,1.243,3.29,3.29,0,0,1-1.837.5,3.458,3.458,0,0,1-2.4-.891,3.433,3.433,0,0,1-1.1-2.409h8.844a3.34,3.34,0,0,0,.088-.572q.022-.308.022-.594A6.154,6.154,0,0,0,48.9-9.251a5.361,5.361,0,0,0-1.936-2.112,5.61,5.61,0,0,0-3.069-.8,5.7,5.7,0,0,0-3,.8,5.773,5.773,0,0,0-2.1,2.2,6.476,6.476,0,0,0-.77,3.179A6.482,6.482,0,0,0,38.8-2.8,5.9,5.9,0,0,0,40.942-.561,5.958,5.958,0,0,0,44.088.264Zm-.2-10.34A3,3,0,0,1,46-9.284a2.9,2.9,0,0,1,.924,2.068h-6.27a3.313,3.313,0,0,1,1.122-2.112A3.208,3.208,0,0,1,43.89-10.076ZM51.546,0h2.486V-16.654H51.546ZM60.9.264a5.6,5.6,0,0,0,2.321-.451,3.635,3.635,0,0,0,1.551-1.254,3.21,3.21,0,0,0,.55-1.859,3.088,3.088,0,0,0-.792-2.123A4.635,4.635,0,0,0,62.26-6.732L60.324-7.3a4.436,4.436,0,0,1-1.034-.484,1.023,1.023,0,0,1-.484-.924,1.212,1.212,0,0,1,.484-1.012,2.068,2.068,0,0,1,1.3-.374,3.005,3.005,0,0,1,1.705.506A2.944,2.944,0,0,1,63.4-8.228l1.914-.9a4.344,4.344,0,0,0-1.8-2.233,5.337,5.337,0,0,0-2.9-.8,5.1,5.1,0,0,0-2.178.451,3.7,3.7,0,0,0-1.518,1.243,3.2,3.2,0,0,0-.55,1.87,3.1,3.1,0,0,0,.759,2.09,4.624,4.624,0,0,0,2.3,1.32l1.87.528a3.923,3.923,0,0,1,1.078.473,1.057,1.057,0,0,1,.506.957,1.259,1.259,0,0,1-.55,1.078,2.391,2.391,0,0,1-1.43.4,3.2,3.2,0,0,1-1.881-.594A4.049,4.049,0,0,1,57.684-3.96l-1.914.9a4.774,4.774,0,0,0,1.925,2.42A5.7,5.7,0,0,0,60.9.264Z"
transform="translate(3959 -2728)"
fill={useColorModeValue('#000', '#fff')}
/>
</g>
</Icon>
)

View File

@ -1,18 +1,46 @@
import { Stack, Text } from '@chakra-ui/react'
import { Button, HStack, Stack } from '@chakra-ui/react'
import { VideoBubbleBlock } from '@typebot.io/schemas'
import { TextInput } from '@/components/inputs'
import { useTranslate } from '@tolgee/react'
import { parseVideoUrl } from '@typebot.io/schemas/features/blocks/bubbles/video/helpers'
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { useState } from 'react'
import { PexelsPicker } from '@/components/VideoUploadContent/PexelsPicker'
import { VideoLinkEmbedContent } from '@/components/VideoUploadContent/VideoLinkEmbedContent'
type Tabs = 'link' | 'pexels'
type Props = {
content?: VideoBubbleBlock['content']
onSubmit: (content: VideoBubbleBlock['content']) => void
initialTab?: Tabs
} & (
| {
includedTabs?: Tabs[]
}
| {
excludedTabs?: Tabs[]
}
)
const defaultDisplayedTabs: Tabs[] = ['link', 'pexels']
export const VideoUploadContent = ({
content,
onSubmit,
initialTab,
...props
}: Props) => {
const includedTabs =
'includedTabs' in props
? props.includedTabs ?? defaultDisplayedTabs
: defaultDisplayedTabs
const excludedTabs = 'excludedTabs' in props ? props.excludedTabs ?? [] : []
const displayedTabs = defaultDisplayedTabs.filter(
(tab) => !excludedTabs.includes(tab) && includedTabs.includes(tab)
)
const [currentTab, setCurrentTab] = useState<Tabs>(
initialTab ?? displayedTabs[0]
)
export const VideoUploadContent = ({ content, onSubmit }: Props) => {
const { t } = useTranslate()
const updateUrl = (url: string) => {
const {
type,
@ -20,6 +48,10 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
id,
videoSizeSuggestion,
} = parseVideoUrl(url)
if (currentTab !== 'link') {
// Allow user to update video settings after selection
setCurrentTab('link')
}
return onSubmit({
...content,
type,
@ -30,94 +62,40 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
: {}),
})
}
const updateAspectRatio = (aspectRatio?: string) => {
return onSubmit({
...content,
aspectRatio,
})
}
const updateMaxWidth = (maxWidth?: string) => {
return onSubmit({
...content,
maxWidth,
})
}
const updateAutoPlay = (isAutoplayEnabled: boolean) => {
return onSubmit({ ...content, isAutoplayEnabled })
}
const updateControlsDisplay = (areControlsDisplayed: boolean) => {
if (areControlsDisplayed === false) {
// Make sure autoplay is enabled when video controls are disabled
return onSubmit({
...content,
isAutoplayEnabled: true,
areControlsDisplayed,
})
}
return onSubmit({ ...content, areControlsDisplayed })
}
return (
<Stack p="2" spacing={4}>
<Stack>
<TextInput
placeholder={t('video.urlInput.placeholder')}
defaultValue={content?.url ?? ''}
onChange={updateUrl}
/>
<Text fontSize="xs" color="gray.400" textAlign="center">
{t('video.urlInput.helperText')}
</Text>
</Stack>
{content?.url && (
<Stack>
<TextInput
label={t('video.aspectRatioInput.label')}
moreInfoTooltip={t('video.aspectRatioInput.moreInfoTooltip')}
defaultValue={
content?.aspectRatio ?? defaultVideoBubbleContent.aspectRatio
}
onChange={updateAspectRatio}
direction="row"
/>
<TextInput
label={t('video.maxWidthInput.label')}
moreInfoTooltip={t('video.maxWidthInput.moreInfoTooltip')}
defaultValue={
content?.maxWidth ?? defaultVideoBubbleContent.maxWidth
}
onChange={updateMaxWidth}
direction="row"
/>
</Stack>
<HStack>
{displayedTabs.includes('link') && (
<Button
variant={currentTab === 'link' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('link')}
size="sm"
>
Link
</Button>
)}
{content?.url && content?.type === 'url' && (
<Stack>
<SwitchWithLabel
label={'Display controls'}
initialValue={
content?.areControlsDisplayed ??
defaultVideoBubbleContent.areControlsDisplayed
}
onCheckChange={updateControlsDisplay}
{displayedTabs.includes('pexels') && (
<Button
variant={currentTab === 'pexels' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('pexels')}
size="sm"
>
Pexels
</Button>
)}
</HStack>
{/* Body content to be displayed below conditionally based on currentTab */}
{currentTab === 'link' && (
<VideoLinkEmbedContent
content={content}
updateUrl={updateUrl}
onSubmit={onSubmit}
/>
<SwitchWithLabel
label={t('editor.blocks.bubbles.audio.settings.autoplay.label')}
initialValue={
content?.isAutoplayEnabled ??
defaultVideoBubbleContent.isAutoplayEnabled
}
isChecked={
content?.isAutoplayEnabled ??
defaultVideoBubbleContent.isAutoplayEnabled
}
isDisabled={content?.areControlsDisplayed === false}
onCheckChange={() => updateAutoPlay(!content.isAutoplayEnabled)}
/>
</Stack>
)}
{currentTab === 'pexels' && (
<PexelsPicker videoSize="medium" onVideoSelect={updateUrl} />
)}
</Stack>
)

View File

@ -235,6 +235,14 @@ Used to search for images. You can create an Unsplash app [here](https://unsplas
| NEXT_PUBLIC_UNSPLASH_APP_NAME | | Unsplash App name |
| NEXT_PUBLIC_UNSPLASH_ACCESS_KEY | | Unsplash API key |
## Pexels (video picker)
Used to search for videos. You can create a Pexels app [here](https://www.pexels.com/api/key/)
| Parameter | Default | Description |
| -------------------------- | ------- | -------------- |
| NEXT_PUBLIC_PEXELS_API_KEY | | Pexels API key |
## Tolgee (i18n contribution dev tool)
<Note>

13
packages/env/env.ts vendored
View File

@ -322,6 +322,17 @@ const unsplashEnv = {
},
}
const pexelsEnv = {
client: {
NEXT_PUBLIC_PEXELS_API_KEY: z.string().min(1).optional(),
},
runtimeEnv: {
NEXT_PUBLIC_PEXELS_API_KEY: getRuntimeVariable(
'NEXT_PUBLIC_PEXELS_API_KEY'
),
},
}
const whatsAppEnv = {
server: {
META_SYSTEM_USER_TOKEN: z.string().min(1).optional(),
@ -440,6 +451,7 @@ export const env = createEnv({
...giphyEnv.client,
...vercelEnv.client,
...unsplashEnv.client,
...pexelsEnv.client,
...sentryEnv.client,
...posthogEnv.client,
...tolgeeEnv.client,
@ -452,6 +464,7 @@ export const env = createEnv({
...giphyEnv.runtimeEnv,
...vercelEnv.runtimeEnv,
...unsplashEnv.runtimeEnv,
...pexelsEnv.runtimeEnv,
...sentryEnv.runtimeEnv,
...posthogEnv.runtimeEnv,
...tolgeeEnv.runtimeEnv,

23
pnpm-lock.yaml generated
View File

@ -238,6 +238,9 @@ importers:
papaparse:
specifier: 5.4.1
version: 5.4.1
pexels:
specifier: ^1.4.0
version: 1.4.0
prettier:
specifier: 2.8.8
version: 2.8.8
@ -969,7 +972,7 @@ importers:
version: 2.8.8
ts-jest:
specifier: 29.0.5
version: 29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
version: 29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(esbuild@0.15.18)(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
tsup:
specifier: 6.5.0
version: 6.5.0(@swc/core@1.3.101)(postcss@8.4.35)(typescript@5.4.5)
@ -4083,6 +4086,7 @@ packages:
'@humanwhocodes/config-array@0.11.14':
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
deprecated: Use @eslint/config-array instead
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
@ -4090,6 +4094,7 @@ packages:
'@humanwhocodes/object-schema@2.0.2':
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
deprecated: Use @eslint/object-schema instead
'@img/sharp-darwin-arm64@0.33.2':
resolution: {integrity: sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==}
@ -8489,6 +8494,7 @@ packages:
glob@7.1.7:
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
deprecated: Glob versions prior to v9 are no longer supported
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
@ -8497,6 +8503,7 @@ packages:
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
@ -8789,6 +8796,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -10607,6 +10615,9 @@ packages:
periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
pexels@1.4.0:
resolution: {integrity: sha512-akpLySokCtw9JHGx7yMavOIAHGVP5721rLUONR/cFKjWkLjUXsHrJ5jndMKss9mx7AEMZRXs7loxEb+vLJf6kA==}
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@ -11417,6 +11428,7 @@ packages:
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rollup-plugin-postcss@4.0.2:
@ -24186,6 +24198,12 @@ snapshots:
estree-walker: 3.0.3
is-reference: 3.0.2
pexels@1.4.0:
dependencies:
isomorphic-fetch: 3.0.0
transitivePeerDependencies:
- encoding
picocolors@1.0.0: {}
picomatch@2.3.1: {}
@ -26026,7 +26044,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-jest@29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5):
ts-jest@29.0.5(@babel/core@7.22.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.22.9))(esbuild@0.15.18)(jest@29.4.1(@types/node@20.4.2)(babel-plugin-macros@3.1.0))(typescript@5.4.5):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@ -26042,6 +26060,7 @@ snapshots:
'@babel/core': 7.22.9
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.22.9)
esbuild: 0.15.18
tsconfck@3.0.3(typescript@5.4.5):
optionalDependencies: