2
0

Add recent section in icon and emoji picker

Closes #536
This commit is contained in:
Baptiste Arnaud
2023-06-20 14:04:33 +02:00
parent 36623930bc
commit eaadc59b1f
12 changed files with 161 additions and 87 deletions

View File

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

View File

@ -5,6 +5,7 @@ import {
HStack,
useColorModeValue,
SimpleGrid,
Text,
} from '@chakra-ui/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { iconNames } from './iconNames'
@ -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)
@ -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
@ -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)}'`)
@ -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>
)
}

View File

@ -21,6 +21,8 @@ const objects = emojis['Objects']
const symbols = emojis['Symbols']
const flags = emojis['Flags']
const localStorageRecentEmojisKey = 'recentEmojis'
export const EmojiSearchableList = ({
onEmojiSelected,
}: {
@ -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
@ -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} />
@ -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}

View File

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

View File

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

View File

@ -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 ?? ''}
@ -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}

View File

@ -87,6 +87,7 @@ export const AvatarForm = ({
p="4"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
w="500px"
>
<ImageUploadContent
filePath={uploadFilePath}

View File

@ -8,6 +8,7 @@ import {
Text,
Image,
Button,
Portal,
} from '@chakra-ui/react'
import { isNotEmpty } from '@typebot.io/lib'
import { Background, BackgroundType } from '@typebot.io/schemas'
@ -57,14 +58,16 @@ export const BackgroundContent = ({
<Button>Select an image</Button>
)}
</PopoverTrigger>
<PopoverContent p="4">
<ImageUploadContent
filePath={`typebots/${typebot?.id}/background`}
defaultUrl={background.content}
onSubmit={handleContentChange}
excludedTabs={['giphy', 'icon']}
/>
</PopoverContent>
<Portal>
<PopoverContent p="4" w="500px">
<ImageUploadContent
filePath={`typebots/${typebot?.id}/background`}
defaultUrl={background.content}
onSubmit={handleContentChange}
excludedTabs={['giphy', 'icon']}
/>
</PopoverContent>
</Portal>
</Popover>
)
default:

View File

@ -41,7 +41,9 @@ test.beforeAll(async () => {
test('can switch between workspaces and access typebot', async ({ page }) => {
await page.goto('/typebots')
await expect(page.locator('text="Pro typebot"')).toBeVisible()
await expect(page.locator('text="Pro typebot"')).toBeVisible({
timeout: 20000,
})
await page.click('text=Pro workspace')
await page.click('text="Starter workspace"')
await expect(page.locator('text="Pro typebot"')).toBeHidden()

View File

@ -19,7 +19,6 @@ import { I18nProvider } from '@/locales'
import { TypebotProvider } from '@/features/editor/providers/TypebotProvider'
import { WorkspaceProvider } from '@/features/workspace/WorkspaceProvider'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import en from '@/locales/en'
const { ToastContainer, toast } = createStandaloneToast(customTheme)