@@ -25,10 +25,12 @@ import { useApiTokens } from '../hooks/useApiTokens'
|
||||
import { ApiTokenFromServer } from '../types'
|
||||
import { parseTimeSince } from '@/helpers/parseTimeSince'
|
||||
import { deleteApiTokenQuery } from '../queries/deleteApiTokenQuery'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
|
||||
type Props = { user: User }
|
||||
|
||||
export const ApiTokensList = ({ user }: Props) => {
|
||||
const scopedT = useScopedI18n('account.apiTokens')
|
||||
const { showToast } = useToast()
|
||||
const { apiTokens, isLoading, mutate } = useApiTokens({
|
||||
userId: user.id,
|
||||
@@ -55,13 +57,10 @@ export const ApiTokensList = ({ user }: Props) => {
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Heading fontSize="2xl">API tokens</Heading>
|
||||
<Text>
|
||||
These tokens allow other apps to control your whole account and
|
||||
typebots. Be careful!
|
||||
</Text>
|
||||
<Heading fontSize="2xl">{scopedT('heading')}</Heading>
|
||||
<Text>{scopedT('description')}</Text>
|
||||
<Flex justifyContent="flex-end">
|
||||
<Button onClick={onCreateOpen}>Create</Button>
|
||||
<Button onClick={onCreateOpen}>{scopedT('createButton.label')}</Button>
|
||||
<CreateTokenModal
|
||||
userId={user.id}
|
||||
isOpen={isCreateOpen}
|
||||
@@ -74,8 +73,8 @@ export const ApiTokensList = ({ user }: Props) => {
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th w="130px">Created</Th>
|
||||
<Th>{scopedT('table.nameHeader')}</Th>
|
||||
<Th w="130px">{scopedT('table.createdHeader')}</Th>
|
||||
<Th w="0" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -91,7 +90,7 @@ export const ApiTokensList = ({ user }: Props) => {
|
||||
variant="outline"
|
||||
onClick={() => setDeletingId(token.id)}
|
||||
>
|
||||
Delete
|
||||
{scopedT('deleteButton.label')}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
@@ -119,11 +118,14 @@ export const ApiTokensList = ({ user }: Props) => {
|
||||
onClose={() => setDeletingId(undefined)}
|
||||
message={
|
||||
<Text>
|
||||
The token <strong>{apiTokens?.find(byId(deletingId))?.name}</strong>{' '}
|
||||
will be permanently deleted, are you sure you want to continue?
|
||||
{scopedT('deleteConfirmationMessage', {
|
||||
tokenName: (
|
||||
<strong>{apiTokens?.find(byId(deletingId))?.name}</strong>
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
confirmButtonLabel="Delete"
|
||||
confirmButtonLabel={scopedT('deleteButton.label')}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -10,61 +10,65 @@ import Image from 'next/image'
|
||||
import lightModeIllustration from 'public/images/light-mode.png'
|
||||
import darkModeIllustration from 'public/images/dark-mode.png'
|
||||
import systemModeIllustration from 'public/images/system-mode.png'
|
||||
|
||||
const appearanceData = [
|
||||
{
|
||||
value: 'light',
|
||||
label: 'Light',
|
||||
image: lightModeIllustration,
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: 'Dark',
|
||||
image: darkModeIllustration,
|
||||
},
|
||||
{
|
||||
value: 'system',
|
||||
label: 'System',
|
||||
image: systemModeIllustration,
|
||||
},
|
||||
]
|
||||
import { useScopedI18n } from '@/locales'
|
||||
|
||||
type Props = {
|
||||
defaultValue: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => (
|
||||
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
||||
<HStack spacing={4} w="full" align="stretch">
|
||||
{appearanceData.map((option) => (
|
||||
<VStack
|
||||
key={option.value}
|
||||
as="label"
|
||||
htmlFor={option.label}
|
||||
cursor="pointer"
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
pb={6}
|
||||
>
|
||||
<VStack spacing={4}>
|
||||
<Image
|
||||
src={option.image}
|
||||
alt="Theme preview"
|
||||
style={{ borderRadius: '0.250rem' }}
|
||||
placeholder="blur"
|
||||
/>
|
||||
<Stack>
|
||||
<Text fontWeight="bold">{option.label}</Text>
|
||||
</Stack>
|
||||
</VStack>
|
||||
export const AppearanceRadioGroup = ({ defaultValue, onChange }: Props) => {
|
||||
const scopedT = useScopedI18n('account.preferences.appearance')
|
||||
|
||||
<Radio value={option.value} id={option.label} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
)
|
||||
const appearanceData = [
|
||||
{
|
||||
value: 'light',
|
||||
label: scopedT('lightLabel'),
|
||||
image: lightModeIllustration,
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: scopedT('darkLabel'),
|
||||
image: darkModeIllustration,
|
||||
},
|
||||
{
|
||||
value: 'system',
|
||||
label: scopedT('systemLabel'),
|
||||
image: systemModeIllustration,
|
||||
},
|
||||
]
|
||||
return (
|
||||
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
||||
<HStack spacing={4} w="full" align="stretch">
|
||||
{appearanceData.map((option) => (
|
||||
<VStack
|
||||
key={option.value}
|
||||
as="label"
|
||||
htmlFor={option.label}
|
||||
cursor="pointer"
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
pb={6}
|
||||
>
|
||||
<VStack spacing={4}>
|
||||
<Image
|
||||
src={option.image}
|
||||
alt="Theme preview"
|
||||
style={{ borderRadius: '0.250rem' }}
|
||||
placeholder="blur"
|
||||
/>
|
||||
<Stack>
|
||||
<Text fontWeight="bold">{option.label}</Text>
|
||||
</Stack>
|
||||
</VStack>
|
||||
|
||||
<Radio value={option.value} id={option.label} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CopyButton } from '@/components/CopyButton'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@@ -31,6 +32,7 @@ export const CreateTokenModal = ({
|
||||
onClose,
|
||||
onNewToken,
|
||||
}: Props) => {
|
||||
const scopedT = useScopedI18n('account.apiTokens.createModal')
|
||||
const [name, setName] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [newTokenValue, setNewTokenValue] = useState<string>()
|
||||
@@ -50,14 +52,14 @@ export const CreateTokenModal = ({
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{newTokenValue ? 'Token Created' : 'Create Token'}
|
||||
{newTokenValue ? scopedT('createdHeading') : scopedT('createHeading')}
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
{newTokenValue ? (
|
||||
<ModalBody as={Stack} spacing="4">
|
||||
<Text>
|
||||
Please copy your token and store it in a safe place.{' '}
|
||||
<strong>For security reasons we cannot show it again.</strong>
|
||||
{scopedT('copyInstruction')}
|
||||
<strong>{scopedT('securityWarning')}</strong>
|
||||
</Text>
|
||||
<InputGroup size="md">
|
||||
<Input readOnly pr="4.5rem" value={newTokenValue} />
|
||||
@@ -68,12 +70,9 @@ export const CreateTokenModal = ({
|
||||
</ModalBody>
|
||||
) : (
|
||||
<ModalBody as="form" onSubmit={createToken}>
|
||||
<Text mb="4">
|
||||
Enter a unique name for your token to differentiate it from other
|
||||
tokens.
|
||||
</Text>
|
||||
<Text mb="4">{scopedT('nameInput.label')}</Text>
|
||||
<Input
|
||||
placeholder="I.e. Zapier, Github, Make.com"
|
||||
placeholder={scopedT('nameInput.placeholder')}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</ModalBody>
|
||||
@@ -82,7 +81,7 @@ export const CreateTokenModal = ({
|
||||
<ModalFooter>
|
||||
{newTokenValue ? (
|
||||
<Button onClick={onClose} colorScheme="blue">
|
||||
Done
|
||||
{scopedT('doneButton.label')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -91,7 +90,7 @@ export const CreateTokenModal = ({
|
||||
isLoading={isSubmitting}
|
||||
onClick={createToken}
|
||||
>
|
||||
Create token
|
||||
{scopedT('createButton.label')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MouseIcon, LaptopIcon } from '@/components/icons'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
import {
|
||||
HStack,
|
||||
Radio,
|
||||
@@ -9,22 +10,6 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { GraphNavigation } from '@typebot.io/prisma'
|
||||
|
||||
const graphNavigationData = [
|
||||
{
|
||||
value: GraphNavigation.MOUSE,
|
||||
label: 'Mouse',
|
||||
description:
|
||||
'Move by dragging the board and zoom in/out using the scroll wheel',
|
||||
icon: <MouseIcon boxSize="35px" />,
|
||||
},
|
||||
{
|
||||
value: GraphNavigation.TRACKPAD,
|
||||
label: 'Trackpad',
|
||||
description: 'Move the board using 2 fingers and zoom in/out by pinching',
|
||||
icon: <LaptopIcon boxSize="35px" />,
|
||||
},
|
||||
]
|
||||
|
||||
type Props = {
|
||||
defaultValue: string
|
||||
onChange: (value: string) => void
|
||||
@@ -32,33 +17,50 @@ type Props = {
|
||||
export const GraphNavigationRadioGroup = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
}: Props) => (
|
||||
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
||||
<HStack spacing={4} w="full" align="stretch">
|
||||
{graphNavigationData.map((option) => (
|
||||
<VStack
|
||||
key={option.value}
|
||||
as="label"
|
||||
htmlFor={option.label}
|
||||
cursor="pointer"
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
p="6"
|
||||
spacing={6}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<VStack spacing={6}>
|
||||
{option.icon}
|
||||
<Stack>
|
||||
<Text fontWeight="bold">{option.label}</Text>
|
||||
<Text>{option.description}</Text>
|
||||
</Stack>
|
||||
</VStack>
|
||||
}: Props) => {
|
||||
const scopedT = useScopedI18n('account.preferences.graphNavigation')
|
||||
const graphNavigationData = [
|
||||
{
|
||||
value: GraphNavigation.MOUSE,
|
||||
label: scopedT('mouse.label'),
|
||||
description: scopedT('mouse.description'),
|
||||
icon: <MouseIcon boxSize="35px" />,
|
||||
},
|
||||
{
|
||||
value: GraphNavigation.TRACKPAD,
|
||||
label: scopedT('trackpad.label'),
|
||||
description: scopedT('trackpad.description'),
|
||||
icon: <LaptopIcon boxSize="35px" />,
|
||||
},
|
||||
]
|
||||
return (
|
||||
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
|
||||
<HStack spacing={4} w="full" align="stretch">
|
||||
{graphNavigationData.map((option) => (
|
||||
<VStack
|
||||
key={option.value}
|
||||
as="label"
|
||||
htmlFor={option.label}
|
||||
cursor="pointer"
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
p="6"
|
||||
spacing={6}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<VStack spacing={6}>
|
||||
{option.icon}
|
||||
<Stack>
|
||||
<Text fontWeight="bold">{option.label}</Text>
|
||||
<Text>{option.description}</Text>
|
||||
</Stack>
|
||||
</VStack>
|
||||
|
||||
<Radio value={option.value} id={option.label} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
)
|
||||
<Radio value={option.value} id={option.label} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import { ApiTokensList } from './ApiTokensList'
|
||||
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
||||
import { useUser } from '../hooks/useUser'
|
||||
import { TextInput } from '@/components/inputs/TextInput'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
|
||||
export const MyAccountForm = () => {
|
||||
const scopedT = useScopedI18n('account.myAccount')
|
||||
const { user, updateUser } = useUser()
|
||||
const [name, setName] = useState(user?.name ?? '')
|
||||
const [email, setEmail] = useState(user?.email ?? '')
|
||||
@@ -41,10 +43,10 @@ export const MyAccountForm = () => {
|
||||
leftIcon={<UploadIcon />}
|
||||
onFileUploaded={handleFileUploaded}
|
||||
>
|
||||
Change photo
|
||||
{scopedT('changePhotoButton.label')}
|
||||
</UploadButton>
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
.jpg or.png, max 1MB
|
||||
{scopedT('changePhotoButton.specification')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
@@ -52,17 +54,17 @@ export const MyAccountForm = () => {
|
||||
<TextInput
|
||||
defaultValue={name}
|
||||
onChange={handleNameChange}
|
||||
label="Name:"
|
||||
label={scopedT('nameInput.label')}
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
<Tooltip label="Updating email is not available. Contact the support if you want to change it.">
|
||||
<Tooltip label={scopedT('emailInput.disabledTooltip')}>
|
||||
<span>
|
||||
<TextInput
|
||||
type="email"
|
||||
defaultValue={email}
|
||||
onChange={handleEmailChange}
|
||||
label="Email address:"
|
||||
label={scopedT('emailInput.label')}
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
isDisabled
|
||||
|
||||
@@ -4,8 +4,10 @@ import React, { useEffect } from 'react'
|
||||
import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup'
|
||||
import { AppearanceRadioGroup } from './AppearanceRadioGroup'
|
||||
import { useUser } from '../hooks/useUser'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
|
||||
export const UserPreferencesForm = () => {
|
||||
const scopedT = useScopedI18n('account.preferences')
|
||||
const { colorMode, setColorMode } = useColorMode()
|
||||
const { user, updateUser } = useUser()
|
||||
|
||||
@@ -26,14 +28,14 @@ export const UserPreferencesForm = () => {
|
||||
return (
|
||||
<Stack spacing={12}>
|
||||
<Stack spacing={6}>
|
||||
<Heading size="md">Editor Navigation</Heading>
|
||||
<Heading size="md">{scopedT('graphNavigation.heading')}</Heading>
|
||||
<GraphNavigationRadioGroup
|
||||
defaultValue={user?.graphNavigation ?? GraphNavigation.TRACKPAD}
|
||||
onChange={changeGraphNavigation}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack spacing={6}>
|
||||
<Heading size="md">Appearance</Heading>
|
||||
<Heading size="md">{scopedT('appearance.heading')}</Heading>
|
||||
<AppearanceRadioGroup
|
||||
defaultValue={
|
||||
user?.preferredAppAppearance
|
||||
|
||||
Reference in New Issue
Block a user