mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-04 14:45:40 +00:00
✨(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:
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,8 +10,9 @@ import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
|||||||
import { ProConnectButton } from '@/components/ProConnectButton'
|
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
import { useCreateRoom } from '@/features/rooms'
|
import { useCreateRoom } from '@/features/rooms'
|
||||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
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 { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||||
|
import { PersonalizeMeetingDialog } from '@/features/home/components/PersonalizeMeetingDialog'
|
||||||
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
||||||
import { MoreLink } from '@/features/home/components/MoreLink'
|
import { MoreLink } from '@/features/home/components/MoreLink'
|
||||||
import { ReactNode, useState } from 'react'
|
import { ReactNode, useState } from 'react'
|
||||||
@@ -155,6 +156,7 @@ export const Home = () => {
|
|||||||
|
|
||||||
const { mutateAsync: createRoom } = useCreateRoom()
|
const { mutateAsync: createRoom } = useCreateRoom()
|
||||||
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
||||||
|
const [isPersonalizeModalOpen, setIsPersonalizeModalOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserAware>
|
<UserAware>
|
||||||
@@ -209,6 +211,18 @@ export const Home = () => {
|
|||||||
<RiLink size={18} />
|
<RiLink size={18} />
|
||||||
{t('createMenu.laterOption')}
|
{t('createMenu.laterOption')}
|
||||||
</MenuItem>
|
</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>
|
</RACMenu>
|
||||||
</Menu>
|
</Menu>
|
||||||
) : (
|
) : (
|
||||||
@@ -250,6 +264,10 @@ export const Home = () => {
|
|||||||
roomId={laterRoomId || ''}
|
roomId={laterRoomId || ''}
|
||||||
onOpenChange={() => setLaterRoomId(null)}
|
onOpenChange={() => setLaterRoomId(null)}
|
||||||
/>
|
/>
|
||||||
|
<PersonalizeMeetingDialog
|
||||||
|
isOpen={isPersonalizeModalOpen}
|
||||||
|
onOpenChange={() => setIsPersonalizeModalOpen(false)}
|
||||||
|
/>
|
||||||
</Screen>
|
</Screen>
|
||||||
</UserAware>
|
</UserAware>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"moreAbout": "",
|
"moreAbout": "",
|
||||||
"createMenu": {
|
"createMenu": {
|
||||||
"laterOption": "",
|
"laterOption": "",
|
||||||
"instantOption": ""
|
"instantOption": "",
|
||||||
|
"personalizeOption": ""
|
||||||
},
|
},
|
||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "",
|
"heading": "",
|
||||||
@@ -24,6 +25,23 @@
|
|||||||
"copied": "",
|
"copied": "",
|
||||||
"permissions": ""
|
"permissions": ""
|
||||||
},
|
},
|
||||||
|
"personalizeMeetingDialog": {
|
||||||
|
"heading": "",
|
||||||
|
"label": "",
|
||||||
|
"submit": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"errors": {
|
||||||
|
"validation": {
|
||||||
|
"length": "",
|
||||||
|
"spaceOrSpecialCharacter": ""
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"taken": "",
|
||||||
|
"unknown": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warning": ""
|
||||||
|
},
|
||||||
"introSlider": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
"label": "",
|
"label": "",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"moreAbout": "about Visio",
|
"moreAbout": "about Visio",
|
||||||
"createMenu": {
|
"createMenu": {
|
||||||
"laterOption": "Create a meeting for a later date",
|
"laterOption": "Create a meeting for a later date",
|
||||||
"instantOption": "Start an instant meeting"
|
"instantOption": "Start an instant meeting",
|
||||||
|
"personalizeOption": "Create a custom link"
|
||||||
},
|
},
|
||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Your connection details",
|
"heading": "Your connection details",
|
||||||
@@ -24,6 +25,23 @@
|
|||||||
"copied": "Link copied to clipboard",
|
"copied": "Link copied to clipboard",
|
||||||
"permissions": "People with this link do not need your permission to join this meeting."
|
"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": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
"label": "previous",
|
"label": "previous",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"moreAbout": "sur Visio",
|
"moreAbout": "sur Visio",
|
||||||
"createMenu": {
|
"createMenu": {
|
||||||
"laterOption": "Créer une réunion pour une date ultérieure",
|
"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": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Vos informations de connexion",
|
"heading": "Vos informations de connexion",
|
||||||
@@ -24,6 +25,23 @@
|
|||||||
"copied": "Lien copié dans le presse-papiers",
|
"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."
|
"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": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
"label": "précédent",
|
"label": "précédent",
|
||||||
|
|||||||
Reference in New Issue
Block a user