Skip to content

Commit

Permalink
⚡ Add recent section in icon and emoji picker
Browse files Browse the repository at this point in the history
Closes #536
  • Loading branch information
baptisteArno committed Jun 20, 2023
1 parent 3662393 commit eaadc59
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 87 deletions.
2 changes: 1 addition & 1 deletion apps/builder/src/components/ColorPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
</Button>
</PopoverTrigger>
<PopoverContent width="170px">
<PopoverArrow bg={displayedValue} />
<PopoverArrow />
<PopoverCloseButton color="white" />
<PopoverHeader
height="100px"
Expand Down
111 changes: 83 additions & 28 deletions apps/builder/src/components/ImageUploadContent/IconPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
HStack,
useColorModeValue,
SimpleGrid,
Text,
} from '@chakra-ui/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { iconNames } from './iconNames'
Expand All @@ -17,6 +18,9 @@ type Props = {
onIconSelected: (url: string) => void
}

const localStorageRecentIconNamesKey = 'recentIconNames'
const localStorageDefaultIconColorKey = 'defaultIconColor'

export const IconPicker = ({ onIconSelected }: Props) => {
const initialIconColor = useColorModeValue('#222222', '#ffffff')
const scrollContainer = useRef<HTMLDivElement>(null)
Expand All @@ -28,11 +32,22 @@ export const IconPicker = ({ onIconSelected }: Props) => {
const [selectedColor, setSelectedColor] = useState(initialIconColor)
const isWhite = useMemo(
() =>
selectedColor.toLowerCase() === '#ffffff' ||
selectedColor.toLowerCase() === '#fff' ||
selectedColor === 'white',
[selectedColor]
initialIconColor === '#222222' &&
(selectedColor.toLowerCase() === '#ffffff' ||
selectedColor.toLowerCase() === '#fff' ||
selectedColor === 'white'),
[initialIconColor, selectedColor]
)
const [recentIconNames, setRecentIconNames] = useState([])

useEffect(() => {
const recentIconNames = localStorage.getItem(localStorageRecentIconNamesKey)
const defaultIconColor = localStorage.getItem(
localStorageDefaultIconColorKey
)
if (recentIconNames) setRecentIconNames(JSON.parse(recentIconNames))
if (defaultIconColor) setSelectedColor(defaultIconColor)
}, [])

useEffect(() => {
if (!bottomElement.current) return
Expand Down Expand Up @@ -70,10 +85,15 @@ export const IconPicker = ({ onIconSelected }: Props) => {

const updateColor = (color: string) => {
if (!color.startsWith('#')) return
localStorage.setItem(localStorageDefaultIconColorKey, color)
setSelectedColor(color)
}

const selectIcon = async (iconName: string) => {
localStorage.setItem(
localStorageRecentIconNamesKey,
JSON.stringify([...new Set([iconName, ...recentIconNames].slice(0, 30))])
)
const svg = await (await fetch(`/icons/${iconName}.svg`)).text()
const dataUri = `data:image/svg+xml;utf8,${svg
.replace('<svg', `<svg fill='${encodeURIComponent(selectedColor)}'`)
Expand All @@ -89,34 +109,69 @@ export const IconPicker = ({ onIconSelected }: Props) => {
onChange={searchIcon}
withVariableButton={false}
/>
<ColorPicker defaultValue={selectedColor} onColorChange={updateColor} />
<ColorPicker value={selectedColor} onColorChange={updateColor} />
</HStack>

<SimpleGrid
spacing={0}
minChildWidth="38px"
overflowY="scroll"
maxH="350px"
ref={scrollContainer}
overflow="scroll"
>
{displayedIconNames.map((iconName) => (
<Button
size="sm"
variant={isWhite ? 'solid' : 'ghost'}
colorScheme={isWhite ? 'blackAlpha' : undefined}
fontSize="xl"
w="38px"
h="38px"
p="2"
key={iconName}
onClick={() => selectIcon(iconName)}
<Stack overflowY="scroll" maxH="350px" ref={scrollContainer} spacing={4}>
{recentIconNames.length > 0 && (
<Stack>
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
RECENT
</Text>
<SimpleGrid
spacing={0}
gridTemplateColumns={`repeat(auto-fill, minmax(38px, 1fr))`}
bgColor={isWhite ? 'gray.400' : undefined}
rounded="md"
>
{recentIconNames.map((iconName) => (
<Button
size="sm"
variant={'ghost'}
fontSize="xl"
w="38px"
h="38px"
p="2"
key={iconName}
onClick={() => selectIcon(iconName)}
>
<Icon name={iconName} color={selectedColor} />
</Button>
))}
</SimpleGrid>
</Stack>
)}
<Stack>
{recentIconNames.length > 0 && (
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
ICONS
</Text>
)}
<SimpleGrid
spacing={0}
gridTemplateColumns={`repeat(auto-fill, minmax(38px, 1fr))`}
bgColor={isWhite ? 'gray.400' : undefined}
rounded="md"
>
<Icon name={iconName} color={selectedColor} />
</Button>
))}
{displayedIconNames.map((iconName) => (
<Button
size="sm"
variant={'ghost'}
fontSize="xl"
w="38px"
h="38px"
p="2"
key={iconName}
onClick={() => selectIcon(iconName)}
>
<Icon name={iconName} color={selectedColor} />
</Button>
))}
</SimpleGrid>
</Stack>

<div ref={bottomElement} />
</SimpleGrid>
</Stack>
</Stack>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const objects = emojis['Objects']
const symbols = emojis['Symbols']
const flags = emojis['Flags']

const localStorageRecentEmojisKey = 'recentEmojis'

export const EmojiSearchableList = ({
onEmojiSelected,
}: {
Expand All @@ -38,6 +40,13 @@ export const EmojiSearchableList = ({
const [filteredSymbols, setFilteredSymbols] = useState(symbols)
const [filteredFlags, setFilteredFlags] = useState(flags)
const [totalDisplayedCategories, setTotalDisplayedCategories] = useState(1)
const [recentEmojis, setRecentEmojis] = useState([])

useEffect(() => {
const recentIconNames = localStorage.getItem(localStorageRecentEmojisKey)
if (!recentIconNames) return
setRecentEmojis(JSON.parse(recentIconNames))
}, [])

useEffect(() => {
if (!bottomElement.current) return
Expand Down Expand Up @@ -85,84 +94,88 @@ export const EmojiSearchableList = ({
setFilteredFlags(flags)
}

const selectEmoji = (emoji: string) => {
localStorage.setItem(
localStorageRecentEmojisKey,
JSON.stringify([...new Set([emoji, ...recentEmojis].slice(0, 30))])
)
onEmojiSelected(emoji)
}

return (
<Stack>
<ClassicInput placeholder="Search..." onChange={handleSearchChange} />
<Stack ref={scrollContainer} overflow="scroll" maxH="350px" spacing={4}>
{recentEmojis.length > 0 && (
<Stack>
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
RECENT
</Text>
<EmojiGrid emojis={recentEmojis} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredPeople.length > 0 && (
<Stack>
<Text fontSize="sm" pl="2">
People
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
PEOPLE
</Text>
<EmojiGrid emojis={filteredPeople} onEmojiClick={onEmojiSelected} />
<EmojiGrid emojis={filteredPeople} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredAnimals.length > 0 && totalDisplayedCategories >= 2 && (
<Stack>
<Text fontSize="sm" pl="2">
Animals & Nature
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
ANIMALS & NATURE
</Text>
<EmojiGrid
emojis={filteredAnimals}
onEmojiClick={onEmojiSelected}
/>
<EmojiGrid emojis={filteredAnimals} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredFood.length > 0 && totalDisplayedCategories >= 3 && (
<Stack>
<Text fontSize="sm" pl="2">
Food & Drink
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
FOOD & DRINK
</Text>
<EmojiGrid emojis={filteredFood} onEmojiClick={onEmojiSelected} />
<EmojiGrid emojis={filteredFood} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredTravel.length > 0 && totalDisplayedCategories >= 4 && (
<Stack>
<Text fontSize="sm" pl="2">
Travel & Places
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
TRAVEL & PLACES
</Text>
<EmojiGrid emojis={filteredTravel} onEmojiClick={onEmojiSelected} />
<EmojiGrid emojis={filteredTravel} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredActivities.length > 0 && totalDisplayedCategories >= 5 && (
<Stack>
<Text fontSize="sm" pl="2">
Activities
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
ACTIVITIES
</Text>
<EmojiGrid
emojis={filteredActivities}
onEmojiClick={onEmojiSelected}
/>
<EmojiGrid emojis={filteredActivities} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredObjects.length > 0 && totalDisplayedCategories >= 6 && (
<Stack>
<Text fontSize="sm" pl="2">
Objects
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
OBJECTS
</Text>
<EmojiGrid
emojis={filteredObjects}
onEmojiClick={onEmojiSelected}
/>
<EmojiGrid emojis={filteredObjects} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredSymbols.length > 0 && totalDisplayedCategories >= 7 && (
<Stack>
<Text fontSize="sm" pl="2">
Symbols
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
SYMBOLS
</Text>
<EmojiGrid
emojis={filteredSymbols}
onEmojiClick={onEmojiSelected}
/>
<EmojiGrid emojis={filteredSymbols} onEmojiClick={selectEmoji} />
</Stack>
)}
{filteredFlags.length > 0 && totalDisplayedCategories >= 8 && (
<Stack>
<Text fontSize="sm" pl="2">
Flags
<Text fontSize="xs" color="gray.400" fontWeight="semibold" pl="2">
FLAGS
</Text>
<EmojiGrid emojis={filteredFlags} onEmojiClick={onEmojiSelected} />
<EmojiGrid emojis={filteredFlags} onEmojiClick={selectEmoji} />
</Stack>
)}
<div ref={bottomElement} />
Expand All @@ -180,7 +193,11 @@ const EmojiGrid = ({
}) => {
const handleClick = (emoji: string) => () => onEmojiClick(emoji)
return (
<SimpleGrid spacing={0} columns={7}>
<SimpleGrid
spacing={0}
gridTemplateColumns={`repeat(auto-fill, minmax(32px, 1fr))`}
rounded="md"
>
{emojis.map((emoji) => (
<GridItem
as={Button}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test.describe('Payment input block', () => {
.locator(`[placeholder="MM / YY"]`)
.fill('12 / 25')
await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill('240')
await page.locator(`text="Pay 30€"`).click()
await page.getByRole('button', { name: 'Pay 30,00 €' }).click()
await expect(
page.locator(`text="Your card has been declined."`)
).toBeVisible()
Expand All @@ -68,7 +68,7 @@ test.describe('Payment input block', () => {
const zipInput = stripePaymentForm(page).getByPlaceholder('90210')
const isZipInputVisible = await zipInput.isVisible()
if (isZipInputVisible) await zipInput.fill('12345')
await page.locator(`text="Pay 30€"`).click()
await page.getByRole('button', { name: 'Pay 30,00 €' }).click()
await expect(page.locator(`text="Success"`)).toBeVisible()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ test.describe('Google Analytics block', () => {
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill('input[placeholder="G-123456..."]', 'G-VWX9WG1TNS')
await page.fill('input[placeholder="Example: Typebot"]', 'Typebot')
await page.fill(
'input[placeholder="Example: Submit email"]',
'Submit email'
)
await page.fill('input[placeholder="Example: conversion"]', 'conversion')
await page.click('text=Advanced')
await page.fill('input[placeholder="Example: Typebot"]', 'Typebot')
await page.fill('input[placeholder="Example: Campaign Z"]', 'Campaign Z')
await page.fill('input[placeholder="Example: 0"]', '0')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const MetadataForm = ({
rounded="md"
/>
</PopoverTrigger>
<PopoverContent p="4">
<PopoverContent p="4" w="400px">
<ImageUploadContent
filePath={`typebots/${typebotId}/favIcon`}
defaultUrl={metadata.favIconUrl ?? ''}
Expand All @@ -85,7 +85,7 @@ export const MetadataForm = ({
rounded="md"
/>
</PopoverTrigger>
<PopoverContent p="4">
<PopoverContent p="4" w="500px">
<ImageUploadContent
filePath={`typebots/${typebotId}/ogImage`}
defaultUrl={metadata.imageUrl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const AvatarForm = ({
p="4"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
w="500px"
>
<ImageUploadContent
filePath={uploadFilePath}
Expand Down
Loading

4 comments on commit eaadc59

@vercel
Copy link

@vercel vercel bot commented on eaadc59 Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-typebot-io.vercel.app
docs-git-main-typebot-io.vercel.app
docs.typebot.io

@vercel
Copy link

@vercel vercel bot commented on eaadc59 Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on eaadc59 Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app
app.typebot.io

@vercel
Copy link

@vercel vercel bot commented on eaadc59 Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

cdd.searchcube.com.sg
chat.missarkansas.org
chatbot.ownacademy.co
chats.maisefetivo.com
criar.somaperuzzo.com
homerun.wpwakanda.com
prenota.aldoemaria.it
sbutton.wpwakanda.com
talk.convobuilder.com
test.leadbooster.help
whats.laracardoso.com
zillabot.saaszilla.co
815639944.21000000.one
83720273.bouclidom.com
chat.thisiscrushhouse.com
chat2.ambassadorelena.com
healthandsafetycourses.uk
sellmyharleylouisiana.com
testbot.sharemyreview.net
typebot-viewer.vercel.app
verfica.botmachine.com.br
ap-help.algorithmpress.com
ap-main.algorithmpress.com
asking.aschenputtel.agency
bcorporate.carlosbusch.com
bot.adventureconsulting.hu
bot2.fusionstarreviews.com
casestudyemb.wpwakanda.com
chat.atlasoutfittersk9.com
configurator.bouclidom.com
demo.virtuesocialmedia.com
gabinete.baleia.formulario
help.atlasoutfittersk9.com
herbalife.barrettamario.it
homepageonly.wpwakanda.com
liveconvert.kandalearn.com
mainmenu1one.wpwakanda.com
newsletter.itshcormeos.com
prenotazione.aldoemaria.it
protocolosecabarriga.store
rsvp.virtuesocialmedia.com
tarian.theiofoundation.org
ted.meujalecobrasil.com.br
type.dericsoncalari.com.br
baleia.eventos.progenbr.com
bot.desafioserrabarriga.fit
bot.pinpointinteractive.com
bot.polychromes-project.com
bot.seidinembroseanchetu.it
chat.semanalimpanome.com.br
designguide.techyscouts.com
liveconvert2.kandalearn.com
poll.mosaicohairboutique.it
presente.empresarias.com.mx
register.algorithmpress.com
sell.sellthemotorhome.co.uk
anamnese.odontopavani.com.br
austin.channelautomation.com
bot.marketingplusmindset.com
bot.seidibergamoseanchetu.it
desabafe.sergiolimajr.com.br
download.venturemarketing.in
open.campus.aalen.university
piazzatorre.barrettamario.it
poll.mosaicohairboutique.com
type.cookieacademyonline.com
upload.atlasoutfittersk9.com
bot.brigadeirosemdrama.com.br
tuttirecepcao.fratucci.com.br
forms.escoladeautomacao.com.br
onboarding.libertydreamcare.ie
recepcao.tutti.fratucci.com.br
type.talitasouzamarques.com.br
agendamento.sergiolimajr.com.br
anamnese.clinicamegasjdr.com.br
bookings.littlepartymonkeys.com
bot.comercializadoraomicron.com
elevateyourmind.groovepages.com
viewer-v2-typebot-io.vercel.app
yourfeedback.comebackreward.com
baleia.testeeventos.progenbr.com
bot.cabin-rentals-of-georgia.net
chat.portaloficialautorizado.com
open.campus.bot.aalen.university
sondaggio.mosaicohairboutique.it
baleia.testegabinete.progenbr.com
gerador.verificadordehospedes.com
personal-trainer.barrettamario.it
sondaggio.mosaicohairboutique.com
preagendamento.sergiolimajr.com.br
studiotecnicoimmobiliaremerelli.it
download.thailandmicespecialist.com
register.thailandmicespecialist.com
bot.studiotecnicoimmobiliaremerelli.it
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
gabinete.baleia.formulario.progenbr.com
chrome-os-inquiry-system.itschromeos.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com

Please sign in to comment.