♻️(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 i18n from 'i18next'
import { FeatureFlags } from '@/features/analytics/enums'
import { LoginButton } from '@/components/LoginButton'
import { useUser } from '@/features/auth'
import { NoAccessView } from './NoAccessView'
export const ScreenRecordingSidePanel = () => {
const { data } = useConfig()
@@ -43,8 +42,6 @@ export const ScreenRecordingSidePanel = () => {
FeatureFlags.ScreenRecording
)
const { isLoggedIn } = useUser()
const { notifyParticipants } = useNotifyParticipants()
const roomId = useRoomId()
@@ -114,7 +111,7 @@ export const ScreenRecordingSidePanel = () => {
posthog.capture('screen-recording-started', {})
}
} catch (error) {
console.error('Failed to handle transcript:', error)
console.error('Failed to handle recording:', error)
setIsLoading(false)
}
}
@@ -130,92 +127,12 @@ export const ScreenRecordingSidePanel = () => {
if (hasFeatureWithoutAdminRights) {
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
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>
<NoAccessView
i18nKeyPrefix="screenRecording"
i18nKey="notAdminOrOwner"
helpArticle={data?.support?.help_article_recording}
imagePath="/assets/intro-slider/4.png"
/>
)
}

View File

@@ -30,8 +30,6 @@ import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig'
import humanizeDuration from 'humanize-duration'
import i18n from 'i18next'
import { useUser } from '@/features/auth'
import { LoginButton } from '@/components/LoginButton'
import { HStack, VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox.tsx'
@@ -40,12 +38,11 @@ import {
SettingsDialogExtendedKey,
useTranscriptionLanguageOptions,
} from '@/features/settings'
import { NoAccessView } from './NoAccessView'
export const TranscriptSidePanel = () => {
const { data } = useConfig()
const { isLoggedIn } = useUser()
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
@@ -153,179 +150,23 @@ export const TranscriptSidePanel = () => {
if (hasFeatureWithoutAdminRights) {
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
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>
<NoAccessView
i18nKeyPrefix="transcript"
i18nKey="notAdminOrOwner"
helpArticle={data?.support?.help_article_transcript}
imagePath="/assets/intro-slider/3.png"
/>
)
}
if (!hasTranscriptAccess) {
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
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>
<NoAccessView
i18nKeyPrefix="transcript"
i18nKey="premium"
helpArticle={data?.support?.help_article_transcript}
imagePath="/assets/intro-slider/3.png"
/>
)
}

View File

@@ -338,6 +338,7 @@
"premium": {
"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.",
"linkMore": "Mehr erfahren",
"login": {
"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."

View File

@@ -338,6 +338,7 @@
"premium": {
"heading": "Premium feature",
"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": {
"heading": "You are not logged in!",
"body": "You must be logged in to use this feature. Please log in, then try again."

View File

@@ -338,6 +338,7 @@
"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.",
"linkMore": "En savoir plus",
"login": {
"heading": "Vous n'êtes pas connecté !",
"body": "Vous devez être connecté pour utiliser cette fonctionnalité. Connectez-vous, puis réessayez."

View File

@@ -338,6 +338,7 @@
"premium": {
"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.",
"linkMore": "Meer informatie",
"login": {
"heading": "Inloggen vereist",
"body": "Alleen de maker van de vergadering of een beheerder kan de transcriptie starten. Log in om uw machtigingen te controleren."