Compare commits

...

9 Commits

Author SHA1 Message Date
lebaudantoine f20842b481 (frontend) allow user register to beta feature
I've created a beta form, allowing interested visitors to
register to be a beta user.

This form is yet created with Grist, could be enhance. I'll
investigate using PostHog.
2024-11-17 00:59:40 +01:00
lebaudantoine 3f3ef34053 💫(frontend) add fadein animation on home
Elements' apparition felt quite harsh.
Add an animation to fade in content.
Used a pre-defined animation in panda configs.
2024-11-17 00:53:21 +01:00
lebaudantoine cf647b872f 💫(frontend) add animation on interaction with slider
Add a minimal animation to enhance micro-interaction with our app.
I've tried being minimal, please feel free to enhance or tune this
animation.
2024-11-17 00:52:16 +01:00
lebaudantoine bc9f8c2239 (frontend) add slide feature in the IntroSlider
Avoided installing a dependency for such small piece of code.
I've implemented a naive component, which allow users explore
the slide presenting key feature of our app.

User experience should be ok. However, I might need to optimize
image format and loading strategy. First 'raw' iteration, gonna
optimize it in the future.
2024-11-17 00:50:26 +01:00
lebaudantoine d6372bfd86 💄(frontend) enhance home page
I've totally rework the homepage, heavily inspired by GMeet.
Goal: make it more pro, and polished.
2024-11-17 00:49:19 +01:00
lebaudantoine 4bbdb6abe6 (frontend) introduce learn more link component
Component providing a 'learn more' feature. Inspired by GMeet.
Naive component.
2024-11-17 00:48:33 +01:00
lebaudantoine 8b7708f3a3 🚸(frontend) update A component hover animation
I found original to hide text decoration on hover. It felt wrong
to me, I proposed a new animation. Please feel free to rework it
or enhance it in the future.
2024-11-17 00:47:38 +01:00
lebaudantoine 73a5511dcd 🚧(frontend) add intro slider component with feature slides
Created a component for an intro slider to showcase app features.
Each slide will include a minimal illustration and concise descriptions.

Work In Progress, Slide logic will be added in upcomming commits.
2024-11-17 00:45:04 +01:00
lebaudantoine 998a62041f 💄(frontend) remove bottom border on header
The bottom border on the header was unnecessary
and removed for a cleaner design.
2024-11-17 00:43:53 +01:00
11 changed files with 564 additions and 77 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

@@ -0,0 +1,241 @@
import firstSlide from '@/assets/intro-slider/1_solo.png'
import secondSlide from '@/assets/intro-slider/2_multiple.png'
import thirdSlide from '@/assets/intro-slider/3_resume.png'
import { styled } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { Button, LinkButton } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// todo - extract in a proper env variable
const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'
const Heading = styled('h2', {
base: {
width: 'fit-content',
marginBottom: 0,
fontSize: '1.5rem',
marginTop: '0.75rem',
lineHeight: '2rem',
maxWidth: '23rem',
textAlign: 'center',
textWrap: 'pretty',
},
})
const Body = styled('p', {
base: {
maxWidth: '23rem',
textAlign: 'center',
textWrap: 'pretty',
lineHeight: '1.2rem',
fontSize: '1rem',
},
})
const Image = styled('img', {
base: {
maxHeight: '330px',
height: '100%',
width: 'fit-content',
},
})
const Dot = styled('div', {
base: {
borderRadius: '50%',
display: 'inline-block',
height: '.375rem',
margin: '0 .25rem',
width: '.375rem',
},
variants: {
selected: {
true: {
backgroundColor: '#000091',
},
false: {
backgroundColor: '#CACAFB',
},
},
},
})
const Container = styled('div', {
base: {
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'space-between',
textAlign: 'center',
},
})
const ButtonVerticalCenter = styled('div', {
base: {
marginTop: '10.3125rem',
transform: 'translateY(-50%)',
},
})
const SlideContainer = styled('div', {
base: {
alignItems: 'stretch',
display: 'flex',
position: 'relative',
},
})
const Slide = styled('div', {
base: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.5rem',
justifyContent: 'start',
minHeight: '550px',
minWidth: '200px',
width: '22.625rem',
},
variants: {
visible: {
true: {
visibility: 'visible',
position: 'static',
},
false: {
visibility: 'hidden',
position: 'absolute',
},
},
},
defaultVariants: {
visible: false,
},
})
const TextAnimation = styled('div', {
base: {
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
gap: '0.5rem',
},
variants: {
visible: {
true: {
opacity: 1,
transform: 'none',
transition: 'opacity ease-in .3s, transform ease-in .3s',
},
false: {
opacity: 0,
transform: 'translateX(-30%)',
},
},
},
})
type Slide = {
key: string
img: string
isAvailableInBeta?: boolean
}
// todo - optimize how images are imported
const SLIDES: Slide[] = [
{
key: 'slide1',
img: firstSlide,
},
{
key: 'slide2',
img: secondSlide,
},
{
key: 'slide3',
img: thirdSlide,
isAvailableInBeta: true,
},
]
export const IntroSlider = () => {
const [slideIndex, setSlideIndex] = useState(0)
const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
const NUMBER_SLIDES = SLIDES.length
return (
<Container>
<div
className={css({
display: 'flex',
flexGrow: 1,
justifyContent: 'center',
})}
>
<div>
<ButtonVerticalCenter>
<Button
square
invisible
aria-label={t('previous.label')}
tooltip={t('previous.tooltip')}
onPress={() => setSlideIndex(slideIndex - 1)}
isDisabled={slideIndex == 0}
>
<RiArrowLeftSLine />
</Button>
</ButtonVerticalCenter>
</div>
<SlideContainer>
{SLIDES.map((slide, index) => (
<Slide visible={index == slideIndex}>
<Image src={slide.img} alt={t(`${slide.key}.imgAlt`)} />
<TextAnimation visible={index == slideIndex}>
<Heading>{t(`${slide.key}.title`)}</Heading>
<Body>{t(`${slide.key}.body`)}</Body>
{slide.isAvailableInBeta && (
<LinkButton
href={BETA_USERS_FORM_URL}
tooltip={t('beta.tooltip')}
variant={'primary'}
size={'sm'}
style={{ marginTop: '1rem', width: 'fit-content' }}
>
{t('beta.text')}
</LinkButton>
)}
</TextAnimation>
</Slide>
))}
</SlideContainer>
<div>
<ButtonVerticalCenter>
<Button
square
invisible
aria-label={t('next.label')}
tooltip={t('next.tooltip')}
onPress={() => setSlideIndex(slideIndex + 1)}
isDisabled={slideIndex == NUMBER_SLIDES - 1}
>
<RiArrowRightSLine />
</Button>
</ButtonVerticalCenter>
</div>
</div>
<div
className={css({
marginTop: '0.5rem',
})}
>
{SLIDES.map((_, index) => (
<Dot key={index} selected={index == slideIndex} />
))}
</div>
</Container>
)
}
@@ -0,0 +1,23 @@
import { A, Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
const MANIFEST_LINK =
'https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/'
export const MoreLink = () => {
const { t } = useTranslation('home')
return (
<Text as={'p'} variant={'sm'} style={{ padding: '1rem 0' }}>
<A
href={MANIFEST_LINK}
target="_blank"
rel="noopener noreferrer"
aria-label={t('moreLinkLabel')}
>
{t('moreLink')}
</A>{' '}
{t('moreAbout')}
</Text>
)
}
+199 -69
View File
@@ -1,10 +1,9 @@
import { useTranslation } from 'react-i18next'
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
import { Button, Menu, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { Button, Menu } from '@/primitives'
import { HStack, styled } from '@/styled-system/jsx'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { Centered } from '@/layout/Centered'
import { generateRoomId } from '@/features/rooms'
import { useUser, UserAware } from '@/features/auth'
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
@@ -14,7 +13,127 @@ import { usePersistentUserChoices } from '@livekit/components-react'
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import { RiAddLine, RiLink } from '@remixicon/react'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { useState } from 'react'
import { IntroSlider } from '@/features/home/components/IntroSlider'
import { MoreLink } from '@/features/home/components/MoreLink'
import { ReactNode, useState } from 'react'
import { css } from '@/styled-system/css'
const Columns = ({ children }: { children?: ReactNode }) => {
return (
<div
className={css({
alignItems: 'center',
display: 'inline-flex',
flexDirection: 'column',
height: '100%',
minHeight: '100%',
justifyContent: 'normal',
padding: '0 1rem',
width: 'calc(100%-2rem)',
opacity: 0,
animation: '.5s ease-in fade 0s forwards',
lg: {
flexDirection: 'row',
justifyContent: 'space-evenly',
width: '100%',
padding: 0,
},
})}
>
{children}
</div>
)
}
const LeftColumn = ({ children }: { children?: ReactNode }) => {
return (
<div
className={css({
alignItems: 'center',
textAlign: 'center',
display: 'inline-flex',
flexDirection: 'column',
flexBasis: 'auto',
flexShrink: 0,
maxWidth: '39rem',
width: '100%',
padding: '1rem 3%',
marginTop: 'auto',
lg: {
margin: 0,
textAlign: 'left',
alignItems: 'flex-start',
flexShrink: '1',
flexBasis: '45rem',
maxWidth: '45rem',
padding: '1em 3em',
},
})}
>
{children}
</div>
)
}
const RightColumn = ({ children }: { children?: ReactNode }) => {
return (
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
overflow: 'hidden',
padding: '1rem 3%',
marginBottom: 'auto',
flexBasis: 'auto',
flexShrink: 0,
maxWidth: '39rem',
lg: {
margin: 0,
flexBasis: '45%',
padding: '1em 3em',
},
})}
>
{children}
</div>
)
}
const Separator = styled('div', {
base: {
borderBottom: '1px solid',
borderColor: '#747775',
marginTop: '2.5rem',
maxWidth: '30rem',
width: '100%',
},
})
const Heading = styled('h1', {
base: {
fontSize: '3rem',
lineHeight: '3.2rem',
letterSpacing: '0',
fontWeight: '500',
fontStyle: 'normal',
fontStretch: 'normal',
fontOpticalSizing: 'auto',
marginBottom: 0,
paddingBottom: '0.75rem',
},
})
const IntroText = styled('div', {
base: {
marginBottom: '3rem',
fontSize: '1.375rem',
lineHeight: '1.8rem',
textWrap: 'pretty',
maxWidth: '28rem',
},
})
export const Home = () => {
const { t } = useTranslation('home')
@@ -30,72 +149,83 @@ export const Home = () => {
return (
<UserAware>
<Screen>
<Centered width="fit-content">
<Text as="h1" variant="display">
{t('heading')}
</Text>
<Text as="p" variant="h3">
{t('intro')}
</Text>
{!isLoggedIn && (
<Text margin="sm" variant="note">
{t('loginToCreateMeeting')}
</Text>
)}
<HStack gap="gutter" alignItems="start">
{isLoggedIn ? (
<Menu>
<Button variant="primary" data-attr="create-meeting">
{t('createMeeting')}
<Columns>
<LeftColumn>
<Heading>{t('heading')}</Heading>
<IntroText>{t('intro')}</IntroText>
<HStack gap="gutter" alignItems="start">
{isLoggedIn ? (
<Menu>
<Button variant="primary" data-attr="create-meeting">
{t('createMeeting')}
</Button>
<RACMenu>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={async () => {
const slug = generateRoomId()
createRoom({ slug, username }).then((data) =>
navigateTo('room', data.slug, {
state: { create: true, initialRoomData: data },
})
)
}}
data-attr="create-option-instant"
>
<RiAddLine size={18} />
{t('createMenu.instantOption')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => {
const slug = generateRoomId()
createRoom({ slug, username }).then((data) =>
setLaterRoomId(data.slug)
)
}}
data-attr="create-option-later"
>
<RiLink size={18} />
{t('createMenu.laterOption')}
</MenuItem>
</RACMenu>
</Menu>
) : (
<ProConnectButton hint={false} />
)}
<DialogTrigger>
<Button
variant="primary"
outline
style={{
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
}}
>
{t('joinMeeting')}
</Button>
<RACMenu>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={async () => {
const slug = generateRoomId()
createRoom({ slug, username }).then((data) =>
navigateTo('room', data.slug, {
state: { create: true, initialRoomData: data },
})
)
}}
data-attr="create-option-instant"
>
<RiAddLine size={18} />
{t('createMenu.instantOption')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => {
const slug = generateRoomId()
createRoom({ slug, username }).then((data) =>
setLaterRoomId(data.slug)
)
}}
data-attr="create-option-later"
>
<RiLink size={18} />
{t('createMenu.laterOption')}
</MenuItem>
</RACMenu>
</Menu>
) : (
<ProConnectButton />
)}
<DialogTrigger>
<Button
variant="primary"
outline
style={{
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
}}
>
{t('joinMeeting')}
</Button>
<JoinMeetingDialog />
</DialogTrigger>
</HStack>
</Centered>
<JoinMeetingDialog />
</DialogTrigger>
</HStack>
<Separator />
<div
className={css({
display: { base: 'none', lg: 'inline' },
})}
>
<MoreLink />
</div>
</LeftColumn>
<RightColumn>
<IntroSlider />
<div
className={css({
display: { base: 'inline', lg: 'none' },
})}
>
<MoreLink />
</div>
</RightColumn>
</Columns>
<LaterMeetingDialog
roomId={laterRoomId || ''}
onOpenChange={() => setLaterRoomId(null)}
-3
View File
@@ -22,9 +22,6 @@ export const Header = () => {
return (
<div
className={css({
borderBottomColor: 'box.border',
borderBottomWidth: 1,
borderBottomStyle: 'solid',
paddingY: 1,
paddingX: 1,
flexShrink: 0,
+32
View File
@@ -10,6 +10,9 @@
"joinMeetingTipContent": "",
"joinMeetingTipHeading": "",
"loginToCreateMeeting": "",
"moreLinkLabel": "",
"moreLink": "",
"moreAbout": "",
"createMenu": {
"laterOption": "",
"instantOption": ""
@@ -20,5 +23,34 @@
"copy": "",
"copied": "",
"permissions": ""
},
"introSlider": {
"previous": {
"label": "",
"tooltip": ""
},
"beta": {
"text": "",
"tooltip": ""
},
"next": {
"label": "",
"tooltip": ""
},
"slide1": {
"title": "",
"body": "",
"imgAlt": ""
},
"slide2": {
"title": "",
"body": "",
"imgAlt": ""
},
"slide3": {
"title": "",
"body": "",
"imgAlt": ""
}
}
}
+34 -2
View File
@@ -1,7 +1,7 @@
{
"createMeeting": "Create a meeting",
"heading": "Welcome in Visio",
"intro": "Work easily, from anywhere.",
"heading": "Simple and Secure Video Conferencing",
"intro": "Communicate and work with ease, without compromising your sovereignty",
"joinInputError": "Use a meeting link or code. Examples:",
"joinInputExample": "URL or 10-letter code",
"joinInputLabel": "Meeting link",
@@ -10,6 +10,9 @@
"joinMeetingTipContent": "You can join a meeting by pasting its full link in the browser's address bar.",
"joinMeetingTipHeading": "Did you know?",
"loginToCreateMeeting": "Login to create a meeting",
"moreLinkLabel": "Learn more - new tab",
"moreLink": "Learn more",
"moreAbout": "about Visio",
"createMenu": {
"laterOption": "Create a meeting for a later date",
"instantOption": "Start an instant meeting"
@@ -20,5 +23,34 @@
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"permissions": "People with this link do not need your permission to join this meeting."
},
"introSlider": {
"previous": {
"label": "previous",
"tooltip": "previous"
},
"next": {
"label": "next",
"tooltip": "next"
},
"beta": {
"text": "Join the beta",
"tooltip": "Fill out the form"
},
"slide1": {
"title": "Try Visio to simplify your daily tasks",
"body": "Discover an intuitive and accessible solution, designed for all public agents, their partners, and much more.",
"imgAlt": "Illustration of a user-friendly and accessible collaboration platform"
},
"slide2": {
"title": "Host group calls without limits",
"body": "Unlimited time meetings, up to 15 participants, with smooth and high-quality communication, no matter the group size.",
"imgAlt": "Image of a virtual meeting with multiple participants collaborating seamlessly"
},
"slide3": {
"title": "Transform your meetings with AI",
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!",
"imgAlt": "Illustration of AI-powered note-taking in a virtual meeting"
}
}
}
+34 -2
View File
@@ -1,7 +1,7 @@
{
"createMeeting": "Créer une réunion",
"heading": "Visio",
"intro": "Collaborez en toute simplicité, où que vous soyez.",
"heading": "Visioconférences simples et sécurisées",
"intro": "Communiquez et travaillez en toute simplicité, sans compromis sur votre souveraineté",
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
"joinInputLabel": "Lien complet ou code de la réunion",
@@ -10,6 +10,9 @@
"joinMeetingTipContent": "Vous pouvez rejoindre une réunion en copiant directement son lien complet dans la barre d'adresse du navigateur.",
"joinMeetingTipHeading": "Astuce",
"loginToCreateMeeting": "Connectez-vous pour créer une réunion",
"moreLinkLabel": "En savoir plus - nouvelle fenêtre",
"moreLink": "En savoir plus",
"moreAbout": "sur Visio",
"createMenu": {
"laterOption": "Créer une réunion pour une date ultérieure",
"instantOption": "Démarrer une réunion instantanée"
@@ -20,5 +23,34 @@
"copy": "Copier le lien de la réunion",
"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."
},
"introSlider": {
"previous": {
"label": "précédent",
"tooltip": "précédent"
},
"next": {
"label": "suivant",
"tooltip": "suivant"
},
"beta": {
"text": "Essayer la beta",
"tooltip": "Accéder au formulaire"
},
"slide1": {
"title": "Essayez Visio pour simplifier votre quotidien",
"body": "Découvrez une solution intuitive et accessible, conçue pour tous les agents publics et leurs partenaires, et bien plus encore.",
"imgAlt": "Illustration d'une plateforme de collaboration simple et accessible"
},
"slide2": {
"title": "Organisez des appels de groupe sans limite",
"body": "Réunions sans limite de temps, jusqu'à 15 participants, avec une communication fluide et de haute qualité, quel que soit le nombre.",
"imgAlt": "Image d'une réunion virtuelle avec plusieurs participants collaborant efficacement"
},
"slide3": {
"title": "Transformez vos réunions avec l'IA",
"body": "Obtenez des transcriptions précises et actionnables, pour booster votre productivité. Fonctionnalité en beta, essayez-la maintenant !",
"imgAlt": "Illustration de prise de notes assistée par l'IA dans une réunion virtuelle"
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ const link = cva({
borderRadius: 2,
transition: 'all 0.2s',
'&[data-hovered]': {
textDecoration: 'none',
textDecorationThickness: '2px',
},
'&[data-pressed]': {
textDecoration: 'underline',