diff --git a/src/frontend/src/features/home/components/PersonalizeMeetingDialog.tsx b/src/frontend/src/features/home/components/PersonalizeMeetingDialog.tsx new file mode 100644 index 00000000..6b2705e3 --- /dev/null +++ b/src/frontend/src/features/home/components/PersonalizeMeetingDialog.tsx @@ -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) => { + const { t } = useTranslation('home', { + keyPrefix: 'personalizeMeetingDialog', + }) + const { mutateAsync: createRoom } = useCreateRoom() + const [roomSlug, setRoomSlug] = useState('') + const [serverErrors, setServerErrors] = useState>([]) + + const onSubmit = (e: FormEvent) => { + 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 ( + + + 0} + value={roomSlug} + onChange={(e) => { + setServerErrors([]) + setRoomSlug(e.toLowerCase()) + }} + className={css({ + display: 'flex', + flexDirection: 'column', + })} + > + +
+ + +
+
+ +
    + {errors.map((error, i) => ( +
  • + + {error} + +
  • + ))} +
+
+
+
+
+ + +
+ +
+ + {t('warning')} + +
+
+ ) +} diff --git a/src/frontend/src/features/home/routes/Home.tsx b/src/frontend/src/features/home/routes/Home.tsx index e6ebc812..faf42d16 100644 --- a/src/frontend/src/features/home/routes/Home.tsx +++ b/src/frontend/src/features/home/routes/Home.tsx @@ -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) + const [isPersonalizeModalOpen, setIsPersonalizeModalOpen] = useState(false) return ( @@ -209,6 +211,18 @@ export const Home = () => { {t('createMenu.laterOption')} + { + setIsPersonalizeModalOpen(true) + }} + data-attr="create-option-personalize" + > + + {t('createMenu.personalizeOption')} + ) : ( @@ -250,6 +264,10 @@ export const Home = () => { roomId={laterRoomId || ''} onOpenChange={() => setLaterRoomId(null)} /> + setIsPersonalizeModalOpen(false)} + /> ) diff --git a/src/frontend/src/locales/de/home.json b/src/frontend/src/locales/de/home.json index 74c011f6..e5eb3b30 100644 --- a/src/frontend/src/locales/de/home.json +++ b/src/frontend/src/locales/de/home.json @@ -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": "", diff --git a/src/frontend/src/locales/en/home.json b/src/frontend/src/locales/en/home.json index 7d17a490..e73e2db7 100644 --- a/src/frontend/src/locales/en/home.json +++ b/src/frontend/src/locales/en/home.json @@ -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", diff --git a/src/frontend/src/locales/fr/home.json b/src/frontend/src/locales/fr/home.json index 22b74062..4f298e80 100644 --- a/src/frontend/src/locales/fr/home.json +++ b/src/frontend/src/locales/fr/home.json @@ -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",