♻️(frontend) refactor recording side panels to reduce code duplication

A lot of duplication existed, so I started factorizing components
now that a proper user experience is clearer.

Without over-abstracting, the first step introduces a reusable
“no access” view with configurable message and image.

This is just the beginning: props passing is still not ideal, but
it’s sufficient to merge and significantly reduce duplication.
This commit is contained in:
lebaudantoine
2025-12-31 16:47:45 +01:00
committed by aleb_the_flash
parent 9ebf2f277b
commit 236245740f
8 changed files with 152 additions and 263 deletions

View File

@@ -0,0 +1,45 @@
import { H, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { LoginButton } from '@/components/LoginButton'
interface LoginPromptProps {
heading: string
body: string
}
export const LoginPrompt = ({ heading, body }: LoginPromptProps) => {
return (
<div
className={css({
backgroundColor: 'primary.50',
borderRadius: '5px',
paddingY: '1rem',
paddingX: '1rem',
marginTop: '1rem',
display: 'flex',
flexDirection: 'column',
})}
>
<H
lvl={3}
className={css({
display: 'flex',
alignItems: 'center',
marginBottom: '0.35rem',
})}
>
{heading}
</H>
<Text variant="smNote" wrap="pretty">
{body}
</Text>
<div
className={css({
marginTop: '1rem',
})}
>
<LoginButton proConnectHint={false} />
</div>
</div>
)
}

View File

@@ -0,0 +1,82 @@
import { A, Div, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { LoginPrompt } from './LoginPrompt'
import { useUser } from '@/features/auth'
interface NoAccessViewProps {
i18nKeyPrefix: string
i18nKey: string
helpArticle?: string
imagePath: string
}
export const NoAccessView = ({
i18nKeyPrefix,
i18nKey,
helpArticle,
imagePath,
}: NoAccessViewProps) => {
const { isLoggedIn } = useUser()
const { t } = useTranslation('rooms', { keyPrefix: i18nKeyPrefix })
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
flexDirection="column"
alignItems="center"
>
<img
src={imagePath}
alt=""
className={css({
minHeight: '309px',
height: '309px',
marginBottom: '1rem',
'@media (max-height: 700px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '45%',
marginBottom: '0.3rem',
},
'@media (max-height: 530px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '40%',
marginBottom: '0.1rem',
},
})}
/>
<Text>{t(`${i18nKey}.heading`)}</Text>
<Text
variant="note"
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t(`${i18nKey}.body`)}
<br />
{helpArticle && (
<A href={helpArticle} target="_blank">
{t(`${i18nKey}.linkMore`)}
</A>
)}
</Text>
{!isLoggedIn && (
<LoginPrompt
heading={t(`${i18nKey}.login.heading`)}
body={t(`${i18nKey}.login.body`)}
/>
)}
</Div>
)
}

View File

@@ -27,8 +27,7 @@ import { useConfig } from '@/api/useConfig'
import humanizeDuration from 'humanize-duration' import humanizeDuration from 'humanize-duration'
import i18n from 'i18next' import i18n from 'i18next'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { LoginButton } from '@/components/LoginButton' import { NoAccessView } from './NoAccessView'
import { useUser } from '@/features/auth'
export const ScreenRecordingSidePanel = () => { export const ScreenRecordingSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
@@ -43,8 +42,6 @@ export const ScreenRecordingSidePanel = () => {
FeatureFlags.ScreenRecording FeatureFlags.ScreenRecording
) )
const { isLoggedIn } = useUser()
const { notifyParticipants } = useNotifyParticipants() const { notifyParticipants } = useNotifyParticipants()
const roomId = useRoomId() const roomId = useRoomId()
@@ -114,7 +111,7 @@ export const ScreenRecordingSidePanel = () => {
posthog.capture('screen-recording-started', {}) posthog.capture('screen-recording-started', {})
} }
} catch (error) { } catch (error) {
console.error('Failed to handle transcript:', error) console.error('Failed to handle recording:', error)
setIsLoading(false) setIsLoading(false)
} }
} }
@@ -130,92 +127,12 @@ export const ScreenRecordingSidePanel = () => {
if (hasFeatureWithoutAdminRights) { if (hasFeatureWithoutAdminRights) {
return ( return (
<Div <NoAccessView
display="flex" i18nKeyPrefix="screenRecording"
overflowY="scroll" i18nKey="notAdminOrOwner"
padding="0 1.5rem" helpArticle={data?.support?.help_article_recording}
flexGrow={1} imagePath="/assets/intro-slider/4.png"
flexDirection="column" />
alignItems="center"
>
<img
src="/assets/intro-slider/4.png"
alt={''}
className={css({
minHeight: '309px',
height: '309px',
marginBottom: '1rem',
'@media (max-height: 700px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '45%',
marginBottom: '0.3rem',
},
'@media (max-height: 530px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '40%',
marginBottom: '0.1rem',
},
})}
/>
<Text>{t('notAdminOrOwner.heading')}</Text>
<Text
variant="note"
wrap="balance"
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t('notAdminOrOwner.body')}
<br />
{data?.support?.help_article_recording && (
<A href={data.support.help_article_recording} target="_blank">
{t('notAdminOrOwner.linkMore')}
</A>
)}
</Text>
{!isLoggedIn && (
<div
className={css({
backgroundColor: 'primary.50',
borderRadius: '5px',
paddingY: '1rem',
paddingX: '1rem',
marginTop: '1rem',
display: 'flex',
flexDirection: 'column',
})}
>
<H
lvl={3}
className={css({
display: 'flex',
alignItems: 'center',
marginBottom: '0.35rem',
})}
>
{t('notAdminOrOwner.login.heading')}
</H>
<Text variant="smNote" wrap="balance">
{t('notAdminOrOwner.login.body')}
</Text>
<div
className={css({
marginTop: '1rem',
})}
>
<LoginButton proConnectHint={false} />
</div>
</div>
)}
</Div>
) )
} }

View File

@@ -30,8 +30,6 @@ import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import humanizeDuration from 'humanize-duration' import humanizeDuration from 'humanize-duration'
import i18n from 'i18next' import i18n from 'i18next'
import { useUser } from '@/features/auth'
import { LoginButton } from '@/components/LoginButton'
import { HStack, VStack } from '@/styled-system/jsx' import { HStack, VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox.tsx' import { Checkbox } from '@/primitives/Checkbox.tsx'
@@ -40,12 +38,11 @@ import {
SettingsDialogExtendedKey, SettingsDialogExtendedKey,
useTranscriptionLanguageOptions, useTranscriptionLanguageOptions,
} from '@/features/settings' } from '@/features/settings'
import { NoAccessView } from './NoAccessView'
export const TranscriptSidePanel = () => { export const TranscriptSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
const { isLoggedIn } = useUser()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' }) const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
@@ -153,179 +150,23 @@ export const TranscriptSidePanel = () => {
if (hasFeatureWithoutAdminRights) { if (hasFeatureWithoutAdminRights) {
return ( return (
<Div <NoAccessView
display="flex" i18nKeyPrefix="transcript"
overflowY="scroll" i18nKey="notAdminOrOwner"
padding="0 1.5rem" helpArticle={data?.support?.help_article_transcript}
flexGrow={1} imagePath="/assets/intro-slider/3.png"
flexDirection="column" />
alignItems="center"
>
<img
src="/assets/intro-slider/3.png"
alt={''}
className={css({
minHeight: '309px',
height: '309px',
marginBottom: '1rem',
'@media (max-height: 700px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '45%',
marginBottom: '0.3rem',
},
'@media (max-height: 530px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '40%',
marginBottom: '0.1rem',
},
})}
/>
<Text>{t('notAdminOrOwner.heading')}</Text>
<Text
variant="note"
wrap="balance"
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t('notAdminOrOwner.body')}
<br />
{data?.support?.help_article_transcript && (
<A href={data.support.help_article_transcript} target="_blank">
{t('notAdminOrOwner.linkMore')}
</A>
)}
</Text>
{!isLoggedIn && (
<div
className={css({
backgroundColor: 'primary.50',
borderRadius: '5px',
paddingY: '1rem',
paddingX: '1rem',
marginTop: '1rem',
display: 'flex',
flexDirection: 'column',
})}
>
<H
lvl={3}
className={css({
display: 'flex',
alignItems: 'center',
marginBottom: '0.35rem',
})}
>
{t('notAdminOrOwner.login.heading')}
</H>
<Text variant="smNote" wrap="balance">
{t('notAdminOrOwner.login.body')}
</Text>
<div
className={css({
marginTop: '1rem',
})}
>
<LoginButton proConnectHint={false} />
</div>
</div>
)}
</Div>
) )
} }
if (!hasTranscriptAccess) { if (!hasTranscriptAccess) {
return ( return (
<Div <NoAccessView
display="flex" i18nKeyPrefix="transcript"
overflowY="scroll" i18nKey="premium"
padding="0 1.5rem" helpArticle={data?.support?.help_article_transcript}
flexGrow={1} imagePath="/assets/intro-slider/3.png"
flexDirection="column" />
alignItems="center"
>
<img
src="/assets/intro-slider/3.png"
alt={''}
className={css({
minHeight: '309px',
height: '309px',
marginBottom: '1rem',
'@media (max-height: 700px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '45%',
marginBottom: '0.3rem',
},
'@media (max-height: 530px)': {
height: 'auto',
minHeight: 'auto',
maxHeight: '40%',
marginBottom: '0.1rem',
},
})}
/>
<Text>{t('premium.heading')}</Text>
<Text
variant="note"
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
'@media (max-height: 700px)': {
marginBottom: '1rem',
},
})}
>
{t('premium.body')}{' '}
{data?.support?.help_article_transcript && (
<A href={data.support.help_article_transcript} target="_blank">
{t('linkMore')}
</A>
)}
</Text>
{!isLoggedIn && (
<div
className={css({
backgroundColor: 'primary.50',
borderRadius: '5px',
paddingY: '1rem',
paddingX: '1rem',
marginTop: '1rem',
display: 'flex',
flexDirection: 'column',
})}
>
<H
lvl={3}
className={css({
display: 'flex',
alignItems: 'center',
marginBottom: '0.35rem',
})}
>
{t('premium.login.heading')}
</H>
<Text variant="smNote">{t('premium.login.body')}</Text>
<div
className={css({
marginTop: '1rem',
})}
>
<LoginButton proConnectHint={false} />
</div>
</div>
)}
</Div>
) )
} }

View File

@@ -338,6 +338,7 @@
"premium": { "premium": {
"heading": "Premium-Funktion", "heading": "Premium-Funktion",
"body": "Diese Funktion ist öffentlichen Bediensteten vorbehalten. Wenn Ihre E-Mail-Adresse nicht autorisiert ist, kontaktieren Sie bitte den Support, um Zugriff zu erhalten.", "body": "Diese Funktion ist öffentlichen Bediensteten vorbehalten. Wenn Ihre E-Mail-Adresse nicht autorisiert ist, kontaktieren Sie bitte den Support, um Zugriff zu erhalten.",
"linkMore": "Mehr erfahren",
"login": { "login": {
"heading": "Anmeldung erforderlich", "heading": "Anmeldung erforderlich",
"body": "Nur der Ersteller des Meetings oder ein Administrator kann die Transkription starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen." "body": "Nur der Ersteller des Meetings oder ein Administrator kann die Transkription starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."

View File

@@ -338,6 +338,7 @@
"premium": { "premium": {
"heading": "Premium feature", "heading": "Premium feature",
"body": "This feature is reserved for public agents. If your email address is not authorized, please contact support to get access.", "body": "This feature is reserved for public agents. If your email address is not authorized, please contact support to get access.",
"linkMore": "Learn more",
"login": { "login": {
"heading": "You are not logged in!", "heading": "You are not logged in!",
"body": "You must be logged in to use this feature. Please log in, then try again." "body": "You must be logged in to use this feature. Please log in, then try again."

View File

@@ -338,6 +338,7 @@
"premium": { "premium": {
"heading": "Fonctionnalité premium", "heading": "Fonctionnalité premium",
"body": "Cette fonctionnalité est réservée aux agents publics. Si votre adresse email nest pas autorisée, contactez le support pour obtenir l'accès.", "body": "Cette fonctionnalité est réservée aux agents publics. Si votre adresse email nest pas autorisée, contactez le support pour obtenir l'accès.",
"linkMore": "En savoir plus",
"login": { "login": {
"heading": "Vous n'êtes pas connecté !", "heading": "Vous n'êtes pas connecté !",
"body": "Vous devez être connecté pour utiliser cette fonctionnalité. Connectez-vous, puis réessayez." "body": "Vous devez être connecté pour utiliser cette fonctionnalité. Connectez-vous, puis réessayez."

View File

@@ -338,6 +338,7 @@
"premium": { "premium": {
"heading": "Premiumfunctie", "heading": "Premiumfunctie",
"body": "Deze functie is voorbehouden aan openbare medewerkers. Als uw e-mailadres niet is toegestaan, neem dan contact op met de support om toegang te krijgen.", "body": "Deze functie is voorbehouden aan openbare medewerkers. Als uw e-mailadres niet is toegestaan, neem dan contact op met de support om toegang te krijgen.",
"linkMore": "Meer informatie",
"login": { "login": {
"heading": "Inloggen vereist", "heading": "Inloggen vereist",
"body": "Alleen de maker van de vergadering of een beheerder kan de transcriptie starten. Log in om uw machtigingen te controleren." "body": "Alleen de maker van de vergadering of een beheerder kan de transcriptie starten. Log in om uw machtigingen te controleren."