♻️(frontend) trying out cleaner way to handle routes
it was a bit cumbersome to navigate by paths accross the app as if one path changes a tiny bit we have to make sure we updated everything everywhere and it's kind of error-prone and cumbersome to build paths by hand. now we have a routes file that describes everything: what is the path to each route, and how to navigate to them. We use a new navigateTo helper that helps us. It could be better typed but i'm a newbie.
This commit is contained in:
@@ -5,12 +5,11 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLang } from 'hoofd'
|
import { useLang } from 'hoofd'
|
||||||
import { Route, Switch } from 'wouter'
|
import { Switch, Route } from 'wouter'
|
||||||
import { HomeRoute } from '@/features/home'
|
import { NotFoundScreen } from './layout/NotFoundScreen'
|
||||||
import { RoomRoute, roomRouteRegex } from '@/features/rooms'
|
|
||||||
import { NotFound } from './routes/NotFound'
|
|
||||||
import './i18n/init'
|
|
||||||
import { RenderIfUserFetched } from './features/auth'
|
import { RenderIfUserFetched } from './features/auth'
|
||||||
|
import { routes } from './routes'
|
||||||
|
import './i18n/init'
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
@@ -22,9 +21,10 @@ function App() {
|
|||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<RenderIfUserFetched>
|
<RenderIfUserFetched>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={HomeRoute} />
|
{Object.entries(routes).map(([, route], i) => (
|
||||||
<Route path={roomRouteRegex} component={RoomRoute} />
|
<Route key={i} path={route.path} component={route.Component} />
|
||||||
<Route component={NotFound} />
|
))}
|
||||||
|
<Route component={NotFoundScreen} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</RenderIfUserFetched>
|
</RenderIfUserFetched>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Field, Ul, H, P, Form, Dialog } from '@/primitives'
|
import { Field, Ul, H, P, Form, Dialog } from '@/primitives'
|
||||||
import { isRoomValid, navigateToRoom } from '@/features/rooms'
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
|
import { isRoomValid } from '@/features/rooms'
|
||||||
|
|
||||||
export const JoinMeetingDialog = () => {
|
export const JoinMeetingDialog = () => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
@@ -8,7 +9,7 @@ export const JoinMeetingDialog = () => {
|
|||||||
<Dialog title={t('joinMeeting')}>
|
<Dialog title={t('joinMeeting')}>
|
||||||
<Form
|
<Form
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
navigateToRoom((data.roomId as string).trim())
|
navigateTo('room', data.roomId as string)
|
||||||
}}
|
}}
|
||||||
submitLabel={t('joinInputSubmit')}
|
submitLabel={t('joinInputSubmit')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { navigateToHome } from './navigation/navigateToHome'
|
|
||||||
export { Home as HomeRoute } from './routes/Home'
|
export { Home as HomeRoute } from './routes/Home'
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { navigate } from 'wouter/use-browser-location'
|
|
||||||
|
|
||||||
export const navigateToHome = () => {
|
|
||||||
navigate(`/`)
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,9 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { DialogTrigger } from 'react-aria-components'
|
import { DialogTrigger } from 'react-aria-components'
|
||||||
import { Button, Div, Text, VerticallyOffCenter } from '@/primitives'
|
import { Button, Div, Text, VerticallyOffCenter } from '@/primitives'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
|
import { generateRoomId } from '@/features/rooms'
|
||||||
import { authUrl, useUser } from '@/features/auth'
|
import { authUrl, useUser } from '@/features/auth'
|
||||||
import { navigateToNewRoom } from '@/features/rooms'
|
|
||||||
import { Screen } from '@/layout/Screen'
|
import { Screen } from '@/layout/Screen'
|
||||||
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
||||||
|
|
||||||
@@ -28,7 +29,11 @@ export const Home = () => {
|
|||||||
<HStack gap="gutter">
|
<HStack gap="gutter">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onPress={isLoggedIn ? () => navigateToNewRoom() : undefined}
|
onPress={
|
||||||
|
isLoggedIn
|
||||||
|
? () => navigateTo('room', generateRoomId())
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
href={isLoggedIn ? undefined : authUrl()}
|
href={isLoggedIn ? undefined : authUrl()}
|
||||||
>
|
>
|
||||||
{isLoggedIn ? t('createMeeting') : t('login', { ns: 'global' })}
|
{isLoggedIn ? t('createMeeting') : t('login', { ns: 'global' })}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { Room, RoomOptions } from "livekit-client";
|
import { Room, RoomOptions } from "livekit-client";
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
import { QueryAware } from '@/layout/QueryAware'
|
import { QueryAware } from '@/layout/QueryAware'
|
||||||
import { navigateToHome } from '@/features/home'
|
|
||||||
import { fetchRoom } from '../api/fetchRoom'
|
import { fetchRoom } from '../api/fetchRoom'
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
@@ -50,7 +50,7 @@ export const Conference = ({
|
|||||||
audio={userConfig.audioEnabled}
|
audio={userConfig.audioEnabled}
|
||||||
video={userConfig.videoEnabled}
|
video={userConfig.videoEnabled}
|
||||||
onDisconnected={() => {
|
onDisconnected={() => {
|
||||||
navigateToHome()
|
navigateTo('home')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VideoConference />
|
<VideoConference />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { navigateToNewRoom } from './navigation/navigateToNewRoom'
|
|
||||||
export { navigateToRoom } from './navigation/navigateToRoom'
|
|
||||||
export { Room as RoomRoute } from './routes/Room'
|
export { Room as RoomRoute } from './routes/Room'
|
||||||
export { roomRouteRegex, isRoomValid } from './utils/isRoomValid'
|
export { roomIdPattern, isRoomValid } from './utils/isRoomValid'
|
||||||
|
export { generateRoomId } from './utils/generateRoomId'
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { navigate } from 'wouter/use-browser-location'
|
|
||||||
import { generateRoomId } from '../utils/generateRoomId'
|
|
||||||
|
|
||||||
export const navigateToNewRoom = () => {
|
|
||||||
navigate(`/${generateRoomId()}`)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { navigate } from 'wouter/use-browser-location'
|
|
||||||
|
|
||||||
export const navigateToRoom = (roomId: string) => {
|
|
||||||
navigate(`/${roomId}`)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
|
export const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
|
||||||
export const roomRouteRegex = new RegExp(`^[/](?<roomId>${roomIdPattern})$`)
|
|
||||||
|
|
||||||
export const isRoomValid = (roomId: string) =>
|
export const isRoomValid = (roomId: string) =>
|
||||||
new RegExp(`^${roomIdPattern}$`).test(roomId)
|
new RegExp(`^${roomIdPattern}$`).test(roomId)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { A, Button, Popover, PopoverList, Text } from '@/primitives'
|
import { A, Button, Popover, PopoverList, Text } from '@/primitives'
|
||||||
import { SettingsButton } from '@/features/settings'
|
import { SettingsButton } from '@/features/settings'
|
||||||
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
import { authUrl, logoutUrl, useUser } from '@/features/auth'
|
||||||
import { useMatchesRoute } from '@/utils/useMatchesRoute'
|
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
||||||
import { Feedback } from '@/components/Feedback'
|
import { Feedback } from '@/components/Feedback'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
|
|||||||
9
src/frontend/src/navigation/getRouteByName.ts
Normal file
9
src/frontend/src/navigation/getRouteByName.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { type RouteName, routes } from '@/routes'
|
||||||
|
|
||||||
|
export const getRouteByName = (routeName: RouteName) => {
|
||||||
|
const route = routes[routeName]
|
||||||
|
if (!route) {
|
||||||
|
throw new Error(`Route "${routeName}" does not exist`)
|
||||||
|
}
|
||||||
|
return route
|
||||||
|
}
|
||||||
21
src/frontend/src/navigation/navigateTo.ts
Normal file
21
src/frontend/src/navigation/navigateTo.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { RouteName } from '@/routes'
|
||||||
|
import { navigate } from 'wouter/use-browser-location'
|
||||||
|
import { getRouteByName } from './getRouteByName'
|
||||||
|
|
||||||
|
export const navigateTo = <S = unknown>(
|
||||||
|
routeName: RouteName,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
params?: any,
|
||||||
|
options?: { replace?: boolean; state?: S }
|
||||||
|
) => {
|
||||||
|
const route = getRouteByName(routeName)
|
||||||
|
const to = route.to
|
||||||
|
? route.to(params)
|
||||||
|
: typeof route.path === 'string'
|
||||||
|
? route.path
|
||||||
|
: null
|
||||||
|
if (!to) {
|
||||||
|
throw new Error(`Can't find path to navigate to for ${routeName}`)
|
||||||
|
}
|
||||||
|
return navigate(to, options)
|
||||||
|
}
|
||||||
7
src/frontend/src/navigation/useMatchesRoute.ts
Normal file
7
src/frontend/src/navigation/useMatchesRoute.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { useRoute } from 'wouter'
|
||||||
|
import { type RouteName, routes } from '../routes'
|
||||||
|
|
||||||
|
export const useMatchesRoute = (route: RouteName) => {
|
||||||
|
const [match] = useRoute(routes[route].path)
|
||||||
|
return match
|
||||||
|
}
|
||||||
27
src/frontend/src/routes.ts
Normal file
27
src/frontend/src/routes.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RoomRoute, roomIdPattern } from '@/features/rooms'
|
||||||
|
import { HomeRoute } from '@/features/home'
|
||||||
|
|
||||||
|
export const routes: Record<
|
||||||
|
'home' | 'room',
|
||||||
|
{
|
||||||
|
name: RouteName
|
||||||
|
path: RegExp | string
|
||||||
|
Component: () => JSX.Element
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
to?: (...args: any[]) => string | URL
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
home: {
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
Component: HomeRoute,
|
||||||
|
},
|
||||||
|
room: {
|
||||||
|
name: 'room',
|
||||||
|
path: new RegExp(`^[/](?<roomId>${roomIdPattern})$`),
|
||||||
|
to: (roomId: string) => `/${roomId.trim()}`,
|
||||||
|
Component: RoomRoute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteName = keyof typeof routes
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { NotFoundScreen } from '@/layout/NotFoundScreen'
|
|
||||||
|
|
||||||
export const NotFound = () => {
|
|
||||||
return <NotFoundScreen />
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useRoute } from 'wouter'
|
|
||||||
import { roomRouteRegex } from '@/features/rooms'
|
|
||||||
|
|
||||||
type RouteName = 'home' | 'room'
|
|
||||||
|
|
||||||
const routeMap = {
|
|
||||||
home: '/',
|
|
||||||
room: roomRouteRegex,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMatchesRoute = (route: RouteName) => {
|
|
||||||
const [match] = useRoute(routeMap[route])
|
|
||||||
if (!(route in routeMap)) {
|
|
||||||
throw new Error(`Route ${route} not found`)
|
|
||||||
}
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user