♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View 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>
)

View File

@@ -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>
)
}

View File

@@ -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} />
)

View File

@@ -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>
</>
)
}

View File

@@ -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

View File

@@ -0,0 +1 @@
export { ImageUploadContent } from './ImageUploadContent'