@ -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"
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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')
|
||||
})
|
||||
|
@ -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}
|
||||
|
@ -87,6 +87,7 @@ export const AvatarForm = ({
|
||||
p="4"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
w="500px"
|
||||
>
|
||||
<ImageUploadContent
|
||||
filePath={uploadFilePath}
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user