mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-09 10:19:26 +00:00
♻️(frontend) rework how we handle screens and layout code
Previously each route rendered its whole layout from a to z. Now each route updates a single common wrapper when the layout changes between pages. Also the Loading, Error, UserFetched, QueryAware code is more explicit and understandable. This introduces valtio as dependency, allowing us to deal with global state easily in a svelte/vue way (reactive mutable state). This limits the boilerplaty-ness of immutable state lib approaches, while keeping rendering optimization better than homemade react contexts.
This commit is contained in:
Generated
+50
-3
@@ -23,6 +23,7 @@
|
||||
"react-aria-components": "1.2.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "14.1.3",
|
||||
"valtio": "1.13.2",
|
||||
"wouter": "3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3643,13 +3644,13 @@
|
||||
"version": "15.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -5134,7 +5135,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
@@ -5282,6 +5283,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/derive-valtio": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz",
|
||||
"integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==",
|
||||
"peerDependencies": {
|
||||
"valtio": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
@@ -8937,6 +8946,11 @@
|
||||
"node": "10.* || >= 12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz",
|
||||
"integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -10101,6 +10115,39 @@
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz",
|
||||
"integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==",
|
||||
"dependencies": {
|
||||
"derive-valtio": "0.1.0",
|
||||
"proxy-compare": "2.6.0",
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/valtio/node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/value-or-function": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"react-aria-components": "1.2.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "14.1.3",
|
||||
"valtio": "1.13.2",
|
||||
"wouter": "3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLang } from 'hoofd'
|
||||
import { Switch, Route } from 'wouter'
|
||||
import { NotFoundScreen } from './layout/NotFoundScreen'
|
||||
import { RenderIfUserFetched } from './features/auth'
|
||||
import { Layout } from './layout/Layout'
|
||||
import { NotFoundScreen } from './components/NotFoundScreen'
|
||||
import { routes } from './routes'
|
||||
import './i18n/init'
|
||||
|
||||
@@ -19,14 +19,14 @@ function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense fallback={null}>
|
||||
<RenderIfUserFetched>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{Object.entries(routes).map(([, route], i) => (
|
||||
<Route key={i} path={route.path} component={route.Component} />
|
||||
))}
|
||||
<Route component={NotFoundScreen} />
|
||||
</Switch>
|
||||
</RenderIfUserFetched>
|
||||
</Layout>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Link } from '@/primitives'
|
||||
import { AProps } from '@/primitives/A'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const BackToHome = ({ size }: { size?: AProps['size'] }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<p>
|
||||
<Link to="/" size={size}>
|
||||
{t('backToHome')}
|
||||
</Link>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useState, useEffect, type ReactNode } from 'react'
|
||||
|
||||
export const DelayedRender = ({
|
||||
children,
|
||||
delay = 500,
|
||||
}: {
|
||||
delay?: number
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const [show, setShow] = useState(false)
|
||||
useEffect(() => {
|
||||
if (delay === 0) {
|
||||
setShow(true)
|
||||
return
|
||||
}
|
||||
const timeout = setTimeout(() => setShow(true), delay)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [delay])
|
||||
if (delay !== 0 && !show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const ErrorScreen = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Screen layout="centered">
|
||||
<CenteredContent title={t('error.heading')} withBackButton />
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Screen, type ScreenProps } from '@/layout/Screen'
|
||||
import { DelayedRender } from './DelayedRender'
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
|
||||
export const LoadingScreen = ({
|
||||
delay = 500,
|
||||
header = undefined,
|
||||
layout = 'centered',
|
||||
}: {
|
||||
delay?: number
|
||||
} & Omit<ScreenProps, 'children'>) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DelayedRender delay={delay}>
|
||||
<Screen layout={layout} header={header}>
|
||||
<CenteredContent>
|
||||
<Center>
|
||||
<p>{t('loading')}</p>
|
||||
</Center>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
</DelayedRender>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const NotFoundScreen = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Screen layout="centered">
|
||||
<CenteredContent title={t('notFound.heading')} withBackButton />
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||
|
||||
/**
|
||||
* Render an error or loading Screen while a given `status` is not a success,
|
||||
* otherwise directly render children.
|
||||
*
|
||||
* `status` matches react query statuses.
|
||||
*
|
||||
* Children usually contain a Screen at some point in the render tree.
|
||||
*/
|
||||
export const QueryAware = ({
|
||||
status,
|
||||
children,
|
||||
}: {
|
||||
status: 'error' | 'pending' | 'success'
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
if (status === 'error') {
|
||||
return <ErrorScreen />
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return <LoadingScreen header={undefined} />
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { LoadingScreen } from '@/layout/LoadingScreen'
|
||||
|
||||
/**
|
||||
* wrapper that renders children only when user info has been actually fetched
|
||||
*
|
||||
* this is helpful to prevent flash of logged-out content for a few milliseconds when user is actually logged in
|
||||
*/
|
||||
export const RenderIfUserFetched = ({ children }: { children: ReactNode }) => {
|
||||
const { isLoggedIn } = useUser()
|
||||
return isLoggedIn !== undefined ? (
|
||||
children
|
||||
) : (
|
||||
<LoadingScreen renderTimeout={1000} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useUser } from '@/features/auth'
|
||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||
|
||||
/**
|
||||
* Renders a loading Screen while user info has not been fetched yet,
|
||||
* otherwise directly render children.
|
||||
*
|
||||
* Children usually contain a Screen at some point in the render tree.
|
||||
*
|
||||
* This is helpful to prevent flash of logged-out content for a few milliseconds when user is actually logged in
|
||||
*/
|
||||
export const UserAware = ({ children }: { children: React.ReactNode }) => {
|
||||
const { isLoggedIn } = useUser()
|
||||
|
||||
return isLoggedIn !== undefined ? (
|
||||
children
|
||||
) : (
|
||||
<LoadingScreen header={false} delay={1000} />
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export { useUser } from './api/useUser'
|
||||
export { authUrl } from './utils/authUrl'
|
||||
export { logoutUrl } from './utils/logoutUrl'
|
||||
export { RenderIfUserFetched } from './components/RenderIfUserFetched'
|
||||
export { UserAware } from './components/UserAware'
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DialogTrigger } from 'react-aria-components'
|
||||
import { Button, Div, Text, VerticallyOffCenter } from '@/primitives'
|
||||
import { Button, Text } from '@/primitives'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import { generateRoomId } from '@/features/rooms'
|
||||
import { authUrl, useUser } from '@/features/auth'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { Centered } from '@/layout/Centered'
|
||||
import { generateRoomId } from '@/features/rooms'
|
||||
import { authUrl, useUser, UserAware } from '@/features/auth'
|
||||
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
||||
|
||||
export const Home = () => {
|
||||
const { t } = useTranslation('home')
|
||||
const { isLoggedIn } = useUser()
|
||||
return (
|
||||
<Screen>
|
||||
<VerticallyOffCenter>
|
||||
<Div margin="auto" width="fit-content">
|
||||
<UserAware>
|
||||
<Screen>
|
||||
<Centered width="fit-content">
|
||||
<Text as="h1" variant="display">
|
||||
{t('heading')}
|
||||
</Text>
|
||||
@@ -49,8 +50,8 @@ export const Home = () => {
|
||||
<JoinMeetingDialog />
|
||||
</DialogTrigger>
|
||||
</HStack>
|
||||
</Div>
|
||||
</VerticallyOffCenter>
|
||||
</Screen>
|
||||
</Centered>
|
||||
</Screen>
|
||||
</UserAware>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { Room, RoomOptions } from 'livekit-client'
|
||||
import { keys } from '@/api/queryKeys'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import { QueryAware } from '@/layout/QueryAware'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { QueryAware } from '@/components/QueryAware'
|
||||
import { fetchRoom } from '../api/fetchRoom'
|
||||
import { InviteDialog } from './InviteDialog'
|
||||
|
||||
@@ -67,24 +68,26 @@ export const Conference = ({
|
||||
|
||||
return (
|
||||
<QueryAware status={status}>
|
||||
<LiveKitRoom
|
||||
room={room}
|
||||
serverUrl={data?.livekit?.url}
|
||||
token={data?.livekit?.token}
|
||||
connect={true}
|
||||
audio={userConfig.audioEnabled}
|
||||
video={userConfig.videoEnabled}
|
||||
>
|
||||
<VideoConference />
|
||||
{showInviteDialog && (
|
||||
<InviteDialog
|
||||
isOpen={showInviteDialog}
|
||||
onOpenChange={setShowInviteDialog}
|
||||
roomId={roomId}
|
||||
onClose={() => setShowInviteDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</LiveKitRoom>
|
||||
<Screen>
|
||||
<LiveKitRoom
|
||||
room={room}
|
||||
serverUrl={data?.livekit?.url}
|
||||
token={data?.livekit?.token}
|
||||
connect={true}
|
||||
audio={userConfig.audioEnabled}
|
||||
video={userConfig.videoEnabled}
|
||||
>
|
||||
<VideoConference />
|
||||
{showInviteDialog && (
|
||||
<InviteDialog
|
||||
isOpen={showInviteDialog}
|
||||
onOpenChange={setShowInviteDialog}
|
||||
roomId={roomId}
|
||||
onClose={() => setShowInviteDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</LiveKitRoom>
|
||||
</Screen>
|
||||
</QueryAware>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Box } from '@/layout/Box'
|
||||
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
|
||||
export const Join = ({
|
||||
onSubmit,
|
||||
@@ -10,15 +11,17 @@ export const Join = ({
|
||||
const { t } = useTranslation('rooms')
|
||||
|
||||
return (
|
||||
<Box title={t('join.heading')} withBackButton>
|
||||
<PreJoin
|
||||
persistUserChoices
|
||||
onSubmit={onSubmit}
|
||||
micLabel={t('join.micLabel')}
|
||||
camLabel={t('join.camlabel')}
|
||||
joinLabel={t('join.joinLabel')}
|
||||
userLabel={t('join.userLabel')}
|
||||
/>
|
||||
</Box>
|
||||
<Screen layout="centered">
|
||||
<CenteredContent title={t('join.heading')}>
|
||||
<PreJoin
|
||||
persistUserChoices
|
||||
onSubmit={onSubmit}
|
||||
micLabel={t('join.micLabel')}
|
||||
camLabel={t('join.camlabel')}
|
||||
joinLabel={t('join.joinLabel')}
|
||||
userLabel={t('join.userLabel')}
|
||||
/>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BoxScreen } from '@/layout/BoxScreen'
|
||||
import { Div, Link, P } from '@/primitives'
|
||||
import { P } from '@/primitives'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
|
||||
export const FeedbackRoute = () => {
|
||||
const { t } = useTranslation('rooms')
|
||||
return (
|
||||
<BoxScreen title={t('feedback.heading')}>
|
||||
<Div textAlign="left">
|
||||
<Screen layout="centered">
|
||||
<CenteredContent title={t('feedback.heading')} withBackButton>
|
||||
<P>{t('feedback.body')}</P>
|
||||
</Div>
|
||||
<Div marginTop={1}>
|
||||
<P>
|
||||
<Link to="/">{t('backToHome', { ns: 'global' })}</Link>
|
||||
</P>
|
||||
</Div>
|
||||
</BoxScreen>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import {
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { useParams } from 'wouter'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { ErrorScreen } from '@/layout/ErrorScreen'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { useUser, UserAware } from '@/features/auth'
|
||||
import { Conference } from '../components/Conference'
|
||||
import { Join } from '../components/Join'
|
||||
|
||||
@@ -25,21 +24,23 @@ export const Room = () => {
|
||||
|
||||
if (!userConfig && !skipJoinScreen) {
|
||||
return (
|
||||
<Screen>
|
||||
<UserAware>
|
||||
<Join onSubmit={setUserConfig} />
|
||||
</Screen>
|
||||
</UserAware>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Conference
|
||||
roomId={roomId}
|
||||
mode={mode}
|
||||
userConfig={{
|
||||
...existingUserChoices,
|
||||
...(skipJoinScreen ? { username: user?.email as string } : {}),
|
||||
...userConfig,
|
||||
}}
|
||||
/>
|
||||
<UserAware>
|
||||
<Conference
|
||||
roomId={roomId}
|
||||
mode={mode}
|
||||
userConfig={{
|
||||
...existingUserChoices,
|
||||
...(skipJoinScreen ? { username: user?.email as string } : {}),
|
||||
...userConfig,
|
||||
}}
|
||||
/>
|
||||
</UserAware>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Box as BoxDiv, H, Link, VerticallyOffCenter } from '@/primitives'
|
||||
|
||||
export type BoxProps = {
|
||||
children?: ReactNode
|
||||
title?: ReactNode
|
||||
withBackButton?: boolean
|
||||
}
|
||||
|
||||
export const Box = ({
|
||||
children,
|
||||
title = '',
|
||||
withBackButton = false,
|
||||
}: BoxProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<VerticallyOffCenter>
|
||||
<BoxDiv type="screen">
|
||||
{!!title && <H lvl={1}>{title}</H>}
|
||||
{children}
|
||||
{!!withBackButton && (
|
||||
<p>
|
||||
<Link to="/" size="sm">
|
||||
{t('backToHome')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</BoxDiv>
|
||||
</VerticallyOffCenter>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Screen } from './Screen'
|
||||
import { Box, type BoxProps } from './Box'
|
||||
|
||||
export const BoxScreen = (props: BoxProps) => {
|
||||
return (
|
||||
<Screen>
|
||||
<Box {...props} />
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Div, VerticallyOffCenter } from '@/primitives'
|
||||
import type { SystemStyleObject } from '../styled-system/types'
|
||||
|
||||
export const Centered = ({
|
||||
width = '38rem',
|
||||
children,
|
||||
}: {
|
||||
width?: SystemStyleObject['width']
|
||||
children?: ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<VerticallyOffCenter>
|
||||
<Div margin="auto" width={width} maxWidth="100%">
|
||||
{children}
|
||||
</Div>
|
||||
</VerticallyOffCenter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BackToHome } from '@/components/BackToHome'
|
||||
import { H } from '@/primitives'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
|
||||
export const CenteredContent = ({
|
||||
title,
|
||||
children,
|
||||
withBackButton,
|
||||
}: {
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
withBackButton?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{!!title && (
|
||||
<Center>
|
||||
<H lvl={1}>{title}</H>
|
||||
</Center>
|
||||
)}
|
||||
{children}
|
||||
{!!withBackButton && (
|
||||
<Center marginTop={2}>
|
||||
<BackToHome size="sm" />
|
||||
</Center>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const ErrorScreen = () => {
|
||||
const { t } = useTranslation()
|
||||
return <BoxScreen title={t('error.heading')} withBackButton />
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const ForbiddenScreen = () => {
|
||||
const { t } = useTranslation()
|
||||
return <BoxScreen title={t('forbidden.heading')} withBackButton />
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Header } from './Header'
|
||||
import { layoutStore } from '@/stores/layout'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
export type Layout = 'fullpage' | 'centered'
|
||||
|
||||
/**
|
||||
* Layout component for the app.
|
||||
*
|
||||
* This component is meant to be used as a wrapper around the whole app.
|
||||
* In a specific page, use the `Screen` component and change its props to change global page layout.
|
||||
*/
|
||||
export const Layout = ({ children }: { children: ReactNode }) => {
|
||||
const layoutSnap = useSnapshot(layoutStore)
|
||||
const showHeader = layoutSnap.showHeader
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'white',
|
||||
color: 'default.text',
|
||||
})}
|
||||
>
|
||||
{showHeader && <Header />}
|
||||
<main
|
||||
className={css({
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
import { Screen } from './Screen'
|
||||
import { VerticallyOffCenter } from '@/primitives'
|
||||
import { Center } from '@/styled-system/jsx'
|
||||
|
||||
export const LoadingScreen = ({
|
||||
asBox = false,
|
||||
renderTimeout = 500,
|
||||
}: {
|
||||
asBox?: boolean
|
||||
renderTimeout?: number
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
// show the loading screen only after a little while to prevent flash of texts
|
||||
const [show, setShow] = useState(false)
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => setShow(true), renderTimeout)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [renderTimeout])
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
const Container = asBox ? BoxScreen : Screen
|
||||
return (
|
||||
<Container>
|
||||
<VerticallyOffCenter>
|
||||
<Center>
|
||||
<p>{t('loading')}</p>
|
||||
</Center>
|
||||
</VerticallyOffCenter>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BoxScreen } from './BoxScreen'
|
||||
|
||||
export const NotFoundScreen = () => {
|
||||
const { t } = useTranslation()
|
||||
return <BoxScreen title={t('notFound.heading')} withBackButton />
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ErrorScreen } from './ErrorScreen'
|
||||
import { LoadingScreen } from './LoadingScreen'
|
||||
import { Screen } from './Screen'
|
||||
|
||||
export const QueryAware = ({
|
||||
status,
|
||||
children,
|
||||
}: {
|
||||
status: 'error' | 'pending' | 'success'
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
if (status === 'error') {
|
||||
return <ErrorScreen />
|
||||
}
|
||||
|
||||
if (status === 'pending') {
|
||||
return <LoadingScreen />
|
||||
}
|
||||
|
||||
return <Screen>{children}</Screen>
|
||||
}
|
||||
@@ -1,35 +1,30 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Header } from './Header'
|
||||
import { layoutStore } from '@/stores/layout'
|
||||
import { Layout } from './Layout'
|
||||
import { useEffect } from 'react'
|
||||
import { Centered } from './Centered'
|
||||
|
||||
export type ScreenProps = {
|
||||
/**
|
||||
* 'fullpage' by default.
|
||||
*/
|
||||
layout?: Layout
|
||||
/**
|
||||
* Show header or not.
|
||||
* True by default. Pass undefined to render the screen without modifying current header visibility
|
||||
*/
|
||||
header?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Screen = ({
|
||||
type,
|
||||
layout = 'fullpage',
|
||||
header = true,
|
||||
children,
|
||||
}: {
|
||||
type?: 'splash'
|
||||
children?: ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: type === 'splash' ? 'white' : 'default.bg',
|
||||
color: 'default.text',
|
||||
})}
|
||||
>
|
||||
{type !== 'splash' && <Header />}
|
||||
<main
|
||||
className={css({
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}: ScreenProps) => {
|
||||
useEffect(() => {
|
||||
if (header !== undefined) {
|
||||
layoutStore.showHeader = header
|
||||
}
|
||||
}, [header])
|
||||
return layout === 'centered' ? <Centered>{children}</Centered> : children
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
showHeader: boolean
|
||||
}
|
||||
|
||||
export const layoutStore = proxy<State>({
|
||||
showHeader: false,
|
||||
})
|
||||
@@ -145,6 +145,7 @@
|
||||
|
||||
[data-lk-theme] .lk-prejoin {
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-lk-theme] .lk-participant-tile {
|
||||
|
||||
Reference in New Issue
Block a user