♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
23
apps/builder/src/components/ImageUploadContent/GiphyLogo.tsx
Normal file
23
apps/builder/src/components/ImageUploadContent/GiphyLogo.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IconProps, Icon } from '@chakra-ui/react'
|
||||
|
||||
export const GiphyLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 163.79999999999998 35" {...props}>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M4 4h20v27H4z" fill="#000" />
|
||||
<g fillRule="nonzero">
|
||||
<path d="M0 3h4v29H0z" fill="#04ff8e" />
|
||||
<path d="M24 11h4v21h-4z" fill="#8e2eff" />
|
||||
<path d="M0 31h28v4H0z" fill="#00c5ff" />
|
||||
<path d="M0 0h16v4H0z" fill="#fff152" />
|
||||
<path d="M24 8V4h-4V0h-4v12h12V8" fill="#ff5b5b" />
|
||||
<path d="M24 16v-4h4" fill="#551c99" />
|
||||
</g>
|
||||
<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"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</Icon>
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Flex, Stack, Text } from '@chakra-ui/react'
|
||||
import { GiphyFetch } from '@giphy/js-fetch-api'
|
||||
import { Grid } from '@giphy/react-components'
|
||||
import { GiphyLogo } from './GiphyLogo'
|
||||
import React, { useState } from 'react'
|
||||
import { env, isEmpty } from 'utils'
|
||||
import { Input } from '../inputs'
|
||||
|
||||
type GiphySearchFormProps = {
|
||||
onSubmit: (url: string) => void
|
||||
}
|
||||
|
||||
const giphyFetch = new GiphyFetch(env('GIPHY_API_KEY') as string)
|
||||
|
||||
export const GiphySearchForm = ({ onSubmit }: GiphySearchFormProps) => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
|
||||
const fetchGifs = (offset: number) =>
|
||||
giphyFetch.search(inputValue, { offset, limit: 10 })
|
||||
|
||||
const fetchGifsTrending = (offset: number) =>
|
||||
giphyFetch.trending({ offset, limit: 10 })
|
||||
|
||||
return isEmpty(env('GIPHY_API_KEY')) ? (
|
||||
<Text>NEXT_PUBLIC_GIPHY_API_KEY is missing in environment</Text>
|
||||
) : (
|
||||
<Stack>
|
||||
<Flex align="center">
|
||||
<Input
|
||||
flex="1"
|
||||
autoFocus
|
||||
placeholder="Search..."
|
||||
onChange={setInputValue}
|
||||
withVariableButton={false}
|
||||
/>
|
||||
<GiphyLogo w="100px" />
|
||||
</Flex>
|
||||
<Flex overflowY="scroll" maxH="400px">
|
||||
<Grid
|
||||
key={inputValue}
|
||||
onGifClick={(gif, e) => {
|
||||
e.preventDefault()
|
||||
onSubmit(gif.images.downsized.url)
|
||||
}}
|
||||
fetchGifs={inputValue === '' ? fetchGifsTrending : fetchGifs}
|
||||
width={475}
|
||||
columns={3}
|
||||
className="my-4"
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
|
||||
import { UploadButton } from './UploadButton'
|
||||
import { GiphySearchForm } from './GiphySearchForm'
|
||||
import { Input } from '../inputs/Input'
|
||||
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
|
||||
|
||||
type Props = {
|
||||
filePath: string
|
||||
includeFileName?: boolean
|
||||
defaultUrl?: string
|
||||
isEmojiEnabled?: boolean
|
||||
isGiphyEnabled?: boolean
|
||||
onSubmit: (url: string) => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export const ImageUploadContent = ({
|
||||
filePath,
|
||||
includeFileName,
|
||||
defaultUrl,
|
||||
onSubmit,
|
||||
isEmojiEnabled = false,
|
||||
isGiphyEnabled = true,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const [currentTab, setCurrentTab] = useState<
|
||||
'link' | 'upload' | 'giphy' | 'emoji'
|
||||
>(isEmojiEnabled ? 'emoji' : 'upload')
|
||||
|
||||
const handleSubmit = (url: string) => {
|
||||
onSubmit(url)
|
||||
onClose && onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<HStack>
|
||||
{isEmojiEnabled && (
|
||||
<Button
|
||||
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('emoji')}
|
||||
size="sm"
|
||||
>
|
||||
Emoji
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('upload')}
|
||||
size="sm"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentTab === 'link' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('link')}
|
||||
size="sm"
|
||||
>
|
||||
Embed link
|
||||
</Button>
|
||||
{isGiphyEnabled && (
|
||||
<Button
|
||||
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('giphy')}
|
||||
size="sm"
|
||||
>
|
||||
Giphy
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<BodyContent
|
||||
filePath={filePath}
|
||||
includeFileName={includeFileName}
|
||||
tab={currentTab}
|
||||
onSubmit={handleSubmit}
|
||||
defaultUrl={defaultUrl}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const BodyContent = ({
|
||||
includeFileName,
|
||||
filePath,
|
||||
tab,
|
||||
defaultUrl,
|
||||
onSubmit,
|
||||
}: {
|
||||
includeFileName?: boolean
|
||||
filePath: string
|
||||
tab: 'upload' | 'link' | 'giphy' | 'emoji'
|
||||
defaultUrl?: string
|
||||
onSubmit: (url: string) => void
|
||||
}) => {
|
||||
switch (tab) {
|
||||
case 'upload':
|
||||
return (
|
||||
<UploadFileContent
|
||||
filePath={filePath}
|
||||
includeFileName={includeFileName}
|
||||
onNewUrl={onSubmit}
|
||||
/>
|
||||
)
|
||||
case 'link':
|
||||
return <EmbedLinkContent defaultUrl={defaultUrl} onNewUrl={onSubmit} />
|
||||
case 'giphy':
|
||||
return <GiphyContent onNewUrl={onSubmit} />
|
||||
case 'emoji':
|
||||
return <EmojiSearchableList onEmojiSelected={onSubmit} />
|
||||
}
|
||||
}
|
||||
|
||||
type ContentProps = { onNewUrl: (url: string) => void }
|
||||
|
||||
const UploadFileContent = ({
|
||||
filePath,
|
||||
includeFileName,
|
||||
onNewUrl,
|
||||
}: ContentProps & { filePath: string; includeFileName?: boolean }) => (
|
||||
<Flex justify="center" py="2">
|
||||
<UploadButton
|
||||
filePath={filePath}
|
||||
onFileUploaded={onNewUrl}
|
||||
includeFileName={includeFileName}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Choose an image
|
||||
</UploadButton>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
const EmbedLinkContent = ({
|
||||
defaultUrl,
|
||||
onNewUrl,
|
||||
}: ContentProps & { defaultUrl?: string }) => (
|
||||
<Stack py="2">
|
||||
<Input
|
||||
placeholder={'Paste the image link...'}
|
||||
onChange={onNewUrl}
|
||||
defaultValue={defaultUrl ?? ''}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
const GiphyContent = ({ onNewUrl }: ContentProps) => (
|
||||
<GiphySearchForm onSubmit={onNewUrl} />
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
import { compressFile } from '@/utils/helpers'
|
||||
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
import { uploadFiles } from 'utils'
|
||||
|
||||
type UploadButtonProps = {
|
||||
filePath: string
|
||||
includeFileName?: boolean
|
||||
onFileUploaded: (url: string) => void
|
||||
} & ButtonProps
|
||||
|
||||
export const UploadButton = ({
|
||||
filePath,
|
||||
includeFileName,
|
||||
onFileUploaded,
|
||||
...props
|
||||
}: UploadButtonProps) => {
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target?.files) return
|
||||
setIsUploading(true)
|
||||
const file = e.target.files[0]
|
||||
const urls = await uploadFiles({
|
||||
files: [
|
||||
{
|
||||
file: await compressFile(file),
|
||||
path: `public/${filePath}${includeFileName ? `/${file.name}` : ''}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
if (urls.length && urls[0]) onFileUploaded(urls[0])
|
||||
setIsUploading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<chakra.input
|
||||
data-testid="file-upload-input"
|
||||
type="file"
|
||||
id="file-input"
|
||||
display="none"
|
||||
onChange={handleInputChange}
|
||||
accept=".jpg, .jpeg, .png, .svg, .gif"
|
||||
/>
|
||||
<Button
|
||||
as="label"
|
||||
size="sm"
|
||||
htmlFor="file-input"
|
||||
cursor="pointer"
|
||||
isLoading={isUploading}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import emojis from './emojiList.json'
|
||||
import emojiTagsData from 'emojilib'
|
||||
import {
|
||||
Stack,
|
||||
SimpleGrid,
|
||||
GridItem,
|
||||
Button,
|
||||
Input as ClassicInput,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, ChangeEvent, useEffect, useRef } from 'react'
|
||||
|
||||
const emojiTags = emojiTagsData as Record<string, string[]>
|
||||
|
||||
const people = emojis['Smileys & Emotion'].concat(emojis['People & Body'])
|
||||
const nature = emojis['Animals & Nature']
|
||||
const food = emojis['Food & Drink']
|
||||
const activities = emojis['Activities']
|
||||
const travel = emojis['Travel & Places']
|
||||
const objects = emojis['Objects']
|
||||
const symbols = emojis['Symbols']
|
||||
const flags = emojis['Flags']
|
||||
|
||||
export const EmojiSearchableList = ({
|
||||
onEmojiSelected,
|
||||
}: {
|
||||
onEmojiSelected: (emoji: string) => void
|
||||
}) => {
|
||||
const scrollContainer = useRef<HTMLDivElement>(null)
|
||||
const bottomElement = useRef<HTMLDivElement>(null)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [filteredPeople, setFilteredPeople] = useState(people)
|
||||
const [filteredAnimals, setFilteredAnimals] = useState(nature)
|
||||
const [filteredFood, setFilteredFood] = useState(food)
|
||||
const [filteredTravel, setFilteredTravel] = useState(travel)
|
||||
const [filteredActivities, setFilteredActivities] = useState(activities)
|
||||
const [filteredObjects, setFilteredObjects] = useState(objects)
|
||||
const [filteredSymbols, setFilteredSymbols] = useState(symbols)
|
||||
const [filteredFlags, setFilteredFlags] = useState(flags)
|
||||
const [totalDisplayedCategories, setTotalDisplayedCategories] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
if (!bottomElement.current) return
|
||||
const observer = new IntersectionObserver(handleObserver, {
|
||||
root: scrollContainer.current,
|
||||
})
|
||||
if (bottomElement.current) observer.observe(bottomElement.current)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bottomElement.current])
|
||||
|
||||
const handleObserver = (entities: IntersectionObserverEntry[]) => {
|
||||
const target = entities[0]
|
||||
if (target.isIntersecting) setTotalDisplayedCategories((c) => c + 1)
|
||||
}
|
||||
|
||||
const handleSearchChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const searchValue = e.target.value
|
||||
if (searchValue.length <= 2 && isSearching) return resetEmojiList()
|
||||
setIsSearching(true)
|
||||
setTotalDisplayedCategories(8)
|
||||
const byTag = (emoji: string) =>
|
||||
emojiTags[emoji].find((tag) => tag.includes(searchValue))
|
||||
setFilteredPeople(people.filter(byTag))
|
||||
setFilteredAnimals(nature.filter(byTag))
|
||||
setFilteredFood(food.filter(byTag))
|
||||
setFilteredTravel(travel.filter(byTag))
|
||||
setFilteredActivities(activities.filter(byTag))
|
||||
setFilteredObjects(objects.filter(byTag))
|
||||
setFilteredSymbols(symbols.filter(byTag))
|
||||
setFilteredFlags(flags.filter(byTag))
|
||||
}
|
||||
|
||||
const resetEmojiList = () => {
|
||||
setTotalDisplayedCategories(1)
|
||||
setIsSearching(false)
|
||||
setFilteredPeople(people)
|
||||
setFilteredAnimals(nature)
|
||||
setFilteredFood(food)
|
||||
setFilteredTravel(travel)
|
||||
setFilteredActivities(activities)
|
||||
setFilteredObjects(objects)
|
||||
setFilteredSymbols(symbols)
|
||||
setFilteredFlags(flags)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<ClassicInput placeholder="Search..." onChange={handleSearchChange} />
|
||||
<Stack ref={scrollContainer} overflow="scroll" maxH="350px" spacing={4}>
|
||||
{filteredPeople.length > 0 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
People
|
||||
</Text>
|
||||
<EmojiGrid emojis={filteredPeople} onEmojiClick={onEmojiSelected} />
|
||||
</Stack>
|
||||
)}
|
||||
{filteredAnimals.length > 0 && totalDisplayedCategories >= 2 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Animals & Nature
|
||||
</Text>
|
||||
<EmojiGrid
|
||||
emojis={filteredAnimals}
|
||||
onEmojiClick={onEmojiSelected}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{filteredFood.length > 0 && totalDisplayedCategories >= 3 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Food & Drink
|
||||
</Text>
|
||||
<EmojiGrid emojis={filteredFood} onEmojiClick={onEmojiSelected} />
|
||||
</Stack>
|
||||
)}
|
||||
{filteredTravel.length > 0 && totalDisplayedCategories >= 4 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Travel & Places
|
||||
</Text>
|
||||
<EmojiGrid emojis={filteredTravel} onEmojiClick={onEmojiSelected} />
|
||||
</Stack>
|
||||
)}
|
||||
{filteredActivities.length > 0 && totalDisplayedCategories >= 5 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Activities
|
||||
</Text>
|
||||
<EmojiGrid
|
||||
emojis={filteredActivities}
|
||||
onEmojiClick={onEmojiSelected}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{filteredObjects.length > 0 && totalDisplayedCategories >= 6 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Objects
|
||||
</Text>
|
||||
<EmojiGrid
|
||||
emojis={filteredObjects}
|
||||
onEmojiClick={onEmojiSelected}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{filteredSymbols.length > 0 && totalDisplayedCategories >= 7 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Symbols
|
||||
</Text>
|
||||
<EmojiGrid
|
||||
emojis={filteredSymbols}
|
||||
onEmojiClick={onEmojiSelected}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{filteredFlags.length > 0 && totalDisplayedCategories >= 8 && (
|
||||
<Stack>
|
||||
<Text fontSize="sm" pl="2">
|
||||
Flags
|
||||
</Text>
|
||||
<EmojiGrid emojis={filteredFlags} onEmojiClick={onEmojiSelected} />
|
||||
</Stack>
|
||||
)}
|
||||
<div ref={bottomElement} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const EmojiGrid = ({
|
||||
emojis,
|
||||
onEmojiClick,
|
||||
}: {
|
||||
emojis: string[]
|
||||
onEmojiClick: (emoji: string) => void
|
||||
}) => {
|
||||
const handleClick = (emoji: string) => () => onEmojiClick(emoji)
|
||||
return (
|
||||
<SimpleGrid spacing={0} columns={7}>
|
||||
{emojis.map((emoji) => (
|
||||
<GridItem
|
||||
as={Button}
|
||||
onClick={handleClick(emoji)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
fontSize="xl"
|
||||
key={emoji}
|
||||
>
|
||||
{emoji}
|
||||
</GridItem>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
1
apps/builder/src/components/ImageUploadContent/index.tsx
Normal file
1
apps/builder/src/components/ImageUploadContent/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { ImageUploadContent } from './ImageUploadContent'
|
||||
Reference in New Issue
Block a user