(frontend) introduce a new option while creating room

Display a "personalized" link modal, inspired by Whereby.
The UX and the UI are still cumberstome, however it's fully
functional and accessible.

It's a "okay" v0, allowing users creating their custom link.
Thus we can study this feature traction, and decide whether or
not prioritize this feature in the UX.
This commit is contained in:
lebaudantoine
2025-01-21 20:32:25 +01:00
parent 7529126ca3
commit 51dfc46e15
5 changed files with 210 additions and 4 deletions
@@ -0,0 +1,134 @@
import { FormEvent, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Dialog, type DialogProps, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { RiSpam2Fill } from '@remixicon/react'
import { navigateTo } from '@/navigation/navigateTo.ts'
import { useCreateRoom } from '@/features/rooms'
import {
FieldError,
Form as RACForm,
Input,
Label,
TextField,
} from 'react-aria-components'
import { css } from '@/styled-system/css'
export const PersonalizeMeetingDialog = ({
isOpen,
...dialogProps
}: Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('home', {
keyPrefix: 'personalizeMeetingDialog',
})
const { mutateAsync: createRoom } = useCreateRoom()
const [roomSlug, setRoomSlug] = useState('')
const [serverErrors, setServerErrors] = useState<Array<string>>([])
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
createRoom({ slug: roomSlug })
.then((data) =>
navigateTo('room', data.slug, {
state: { create: true, initialRoomData: data },
})
)
.catch((e) => {
const msg =
e.statusCode === 400
? t('errors.server.taken')
: t('errors.server.unknown')
setServerErrors([msg])
})
}
const validationErrors = []
if (roomSlug.length < 5) {
validationErrors.push(t('errors.validation.length'))
}
if (!roomSlug.match(/^[a-z0-9]+$/)) {
validationErrors.push(t('errors.validation.spaceOrSpecialCharacter'))
}
const errors = [...validationErrors, ...serverErrors]
return (
<Dialog isOpen={!!isOpen} {...dialogProps} title={t('heading')}>
<RACForm onSubmit={onSubmit}>
<TextField
isRequired
isInvalid={errors.length > 0}
value={roomSlug}
onChange={(e) => {
setServerErrors([])
setRoomSlug(e.toLowerCase())
}}
className={css({
display: 'flex',
flexDirection: 'column',
})}
>
<Label>{t('label')}</Label>
<div
className={css({
display: 'flex',
justifyContent: 'space-between',
})}
>
<Input
className={css({
height: '46px',
border: '1px solid black',
padding: '4px 8px',
width: '80%',
borderRadius: '0.25rem',
})}
placeholder={t('placeholder')}
/>
<Button type="submit">{t('submit')}</Button>
</div>
<div
className={css({
minHeight: '72px',
})}
>
<FieldError>
<ul
className={css({
listStyle: 'square inside',
color: 'red',
marginLeft: '10px',
marginTop: '10px',
})}
>
{errors.map((error, i) => (
<li key={i}>
<Text as="span" variant={'sm'}>
{error}
</Text>
</li>
))}
</ul>
</FieldError>
</div>
</TextField>
</RACForm>
<HStack>
<div
style={{
backgroundColor: '#d9e5ff',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
}}
>
<RiSpam2Fill size={22} style={{ fill: '#4c84fc' }} />
</div>
<Text variant="sm" style={{ marginTop: '1rem', textWrap: 'balance' }}>
{t('warning')}
</Text>
</HStack>
</Dialog>
)
}
+19 -1
View File
@@ -10,8 +10,9 @@ import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
import { ProConnectButton } from '@/components/ProConnectButton'
import { useCreateRoom } from '@/features/rooms'
import { usePersistentUserChoices } from '@livekit/components-react'
import { RiAddLine, RiLink } from '@remixicon/react'
import { RiAddLine, RiLink, RiUserAddLine } from '@remixicon/react'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { PersonalizeMeetingDialog } from '@/features/home/components/PersonalizeMeetingDialog'
import { IntroSlider } from '@/features/home/components/IntroSlider'
import { MoreLink } from '@/features/home/components/MoreLink'
import { ReactNode, useState } from 'react'
@@ -155,6 +156,7 @@ export const Home = () => {
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
const [isPersonalizeModalOpen, setIsPersonalizeModalOpen] = useState(false)
return (
<UserAware>
@@ -209,6 +211,18 @@ export const Home = () => {
<RiLink size={18} />
{t('createMenu.laterOption')}
</MenuItem>
<MenuItem
className={
menuRecipe({ icon: true, variant: 'light' }).item
}
onAction={() => {
setIsPersonalizeModalOpen(true)
}}
data-attr="create-option-personalize"
>
<RiUserAddLine size={18} />
{t('createMenu.personalizeOption')}
</MenuItem>
</RACMenu>
</Menu>
) : (
@@ -250,6 +264,10 @@ export const Home = () => {
roomId={laterRoomId || ''}
onOpenChange={() => setLaterRoomId(null)}
/>
<PersonalizeMeetingDialog
isOpen={isPersonalizeModalOpen}
onOpenChange={() => setIsPersonalizeModalOpen(false)}
/>
</Screen>
</UserAware>
)
+19 -1
View File
@@ -15,7 +15,8 @@
"moreAbout": "",
"createMenu": {
"laterOption": "",
"instantOption": ""
"instantOption": "",
"personalizeOption": ""
},
"laterMeetingDialog": {
"heading": "",
@@ -24,6 +25,23 @@
"copied": "",
"permissions": ""
},
"personalizeMeetingDialog": {
"heading": "",
"label": "",
"submit": "",
"placeholder": "",
"errors": {
"validation": {
"length": "",
"spaceOrSpecialCharacter": ""
},
"server": {
"taken": "",
"unknown": ""
}
},
"warning": ""
},
"introSlider": {
"previous": {
"label": "",
+19 -1
View File
@@ -15,7 +15,8 @@
"moreAbout": "about Visio",
"createMenu": {
"laterOption": "Create a meeting for a later date",
"instantOption": "Start an instant meeting"
"instantOption": "Start an instant meeting",
"personalizeOption": "Create a custom link"
},
"laterMeetingDialog": {
"heading": "Your connection details",
@@ -24,6 +25,23 @@
"copied": "Link copied to clipboard",
"permissions": "People with this link do not need your permission to join this meeting."
},
"personalizeMeetingDialog": {
"heading": "Your custom link",
"label": "Your custom link",
"submit": "Create",
"placeholder": "standupbizdev",
"errors": {
"validation": {
"length": "Must contain at least 5 characters.",
"spaceOrSpecialCharacter": "Cannot contain spaces or special characters"
},
"server": {
"taken": "Already in use, please try something else",
"unknown": "An unknown error occurred, please try again"
}
},
"warning": "This link provides direct access to the meeting. Choose a long and complex name to limit unauthorized access."
},
"introSlider": {
"previous": {
"label": "previous",
+19 -1
View File
@@ -15,7 +15,8 @@
"moreAbout": "sur Visio",
"createMenu": {
"laterOption": "Créer une réunion pour une date ultérieure",
"instantOption": "Démarrer une réunion instantanée"
"instantOption": "Démarrer une réunion instantanée",
"personalizeOption": "Créer un lien personnalisé"
},
"laterMeetingDialog": {
"heading": "Vos informations de connexion",
@@ -24,6 +25,23 @@
"copied": "Lien copié dans le presse-papiers",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
},
"personalizeMeetingDialog": {
"heading": "Votre lien personnalisé",
"label": "Votre lien personnalisé",
"submit": "Créer",
"placeholder": "standupbizdev",
"errors": {
"validation": {
"length": "Doit contenir au moins 5 caractères.",
"spaceOrSpecialCharacter": "Ne peut pas contenir d'espaces ou de caractères spéciaux"
},
"server": {
"taken": "Déjà utilisé, veuillez essayer autre chose",
"unknown": "Une erreur inconnue s'est produite, veuillez réessayer"
}
},
"warning": "Ce lien permet un accès direct à la réunion. Choisissez un nom long et complexe pour limiter les accès non autorisés."
},
"introSlider": {
"previous": {
"label": "précédent",