diff --git a/messages/renderer/en.json b/messages/renderer/en.json index 738a477..a7e7ccc 100644 --- a/messages/renderer/en.json +++ b/messages/renderer/en.json @@ -238,5 +238,11 @@ }, "screens.ProjectCreationScreen.title": { "message": "Create a Project" + }, + "tabBar.label.about": { + "message": "About" + }, + "tabBar.label.settings": { + "message": "Settings" } } diff --git a/package-lock.json b/package-lock.json index 9b65cab..5692a26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@tanstack/router-plugin": "^1.81.9", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.1.0", + "@testing-library/user-event": "14.5.2", "@types/eslint__js": "^8.42.3", "@types/lint-staged": "^13.3.0", "@types/node": "^20.17.6", @@ -4334,6 +4335,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/src/renderer/src/Theme.ts b/src/renderer/src/Theme.ts index 81c16f4..62dc11b 100644 --- a/src/renderer/src/Theme.ts +++ b/src/renderer/src/Theme.ts @@ -86,6 +86,7 @@ const theme = createTheme({ }, }, }, + spacing: 1, }) export { theme } diff --git a/src/renderer/src/components/Tabs.tsx b/src/renderer/src/components/Tabs.tsx new file mode 100644 index 0000000..3d3be44 --- /dev/null +++ b/src/renderer/src/components/Tabs.tsx @@ -0,0 +1,91 @@ +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' +import PostAddIcon from '@mui/icons-material/PostAdd' +import SettingsIcon from '@mui/icons-material/Settings' +import Tab from '@mui/material/Tab' +import MuiTabs from '@mui/material/Tabs' +import { styled } from '@mui/material/styles' +import { useLocation, useNavigate } from '@tanstack/react-router' +import { defineMessages, useIntl } from 'react-intl' + +import type { FileRoutesById } from '../routeTree.gen' +import { Text } from './Text' + +const m = defineMessages({ + setting: { + id: 'tabBar.label.settings', + defaultMessage: 'Settings', + }, + about: { + id: 'tabBar.label.about', + defaultMessage: 'About', + }, +}) + +const MapTabStyled = styled(MapTab)({ + width: 60, + '& MuiButtonBase.Mui-selected': { color: '#000' }, +}) + +export const Tabs = () => { + const navigate = useNavigate() + const { formatMessage } = useIntl() + const location = useLocation() + return ( + navigate({ to: value as MapTabRoute })} + orientation="vertical" + value={location.pathname} + TabIndicatorProps={{ style: { backgroundColor: 'transparent' } }} + > + } + value={'/tab1'} + /> + {/* This is needed to properly space the items. Originally used a div, but was causing console errors as the Parent component passes it props, which were invalid for non-tab components */} + + + } + label={ + + {formatMessage(m.setting)} + + } + value={'/tab2'} + /> + } + label={ + + {formatMessage(m.about)} + + } + value={'/tab2'} + /> + + ) +} + +type TabProps = React.ComponentProps + +type MapTabRoute = { + [K in keyof FileRoutesById]: K extends `${'/(MapTabs)/_Map'}${infer Rest}` + ? Rest extends '' + ? never + : `${Rest}` + : never +}[keyof FileRoutesById] + +type MapTabProps = Omit & { value: MapTabRoute } + +function MapTab(props: MapTabProps) { + return +} diff --git a/src/renderer/src/queries/deviceInfo.ts b/src/renderer/src/queries/deviceInfo.ts index 034b7f6..35c5115 100644 --- a/src/renderer/src/queries/deviceInfo.ts +++ b/src/renderer/src/queries/deviceInfo.ts @@ -1,4 +1,8 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query' import { useApi } from '../contexts/ApiContext' @@ -7,7 +11,7 @@ export const DEVICE_INFO_KEY = 'deviceInfo' export const useDeviceInfo = () => { const api = useApi() - return useQuery({ + return useSuspenseQuery({ queryKey: [DEVICE_INFO_KEY], queryFn: async () => { return await api.getDeviceInfo() diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx b/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx index eb9a6a8..cd83f30 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.tab1.tsx @@ -4,10 +4,10 @@ import { createFileRoute } from '@tanstack/react-router' import { Text } from '../../components/Text' export const Route = createFileRoute('/(MapTabs)/_Map/tab1')({ - component: RouteComponent, + component: Observations, }) -function RouteComponent() { +export function Observations() { return (
Tab 1 diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx b/src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx index 91894cc..5171d08 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.tab2.tsx @@ -4,10 +4,10 @@ import { createFileRoute } from '@tanstack/react-router' import { Text } from '../../components/Text' export const Route = createFileRoute('/(MapTabs)/_Map/tab2')({ - component: RouteComponent, + component: Settings, }) -function RouteComponent() { +export function Settings() { return (
Tab 2 diff --git a/src/renderer/src/routes/(MapTabs)/_Map.test.tsx b/src/renderer/src/routes/(MapTabs)/_Map.test.tsx index cf98d8d..34a7182 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.test.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.test.tsx @@ -1,20 +1,54 @@ +import type { ReactNode } from 'react' +import { + RouterProvider, + createRootRoute, + createRoute, + createRouter, +} from '@tanstack/react-router' import { render, screen } from '@testing-library/react' -import { expect, test, vi } from 'vitest' +import { expect, test } from 'vitest' +import { IntlProvider } from '../../contexts/IntlContext' import { MapLayout } from './_Map' -vi.mock('@tanstack/react-router', () => ({ - useNavigate: vi.fn(() => { - return { navigate: vi.fn() } - }), - createFileRoute: vi.fn(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (options: any) => ({ component: options.component }) // Mocked implementation - }), - Outlet: () =>
Mocked Outlet
, -})) - -test('renders something in the jsdom', () => { - render() - expect(screen).toBeDefined() +const rootRoute = createRootRoute({}) + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +) + +// Creates a stubbed out router. We are just testing whether the navigation gets passed the correct route (aka "/tab1" or "/tab2") so we do not need the actual router and can just intecept the navgiation state. +const mapRoute = createRoute({ + getParentRoute: () => rootRoute, + id: 'map', + component: MapLayout, +}) + +const catchAllRoute = createRoute({ + getParentRoute: () => mapRoute, + path: '$', + component: () => null, +}) + +const routeTree = rootRoute.addChildren([mapRoute.addChildren([catchAllRoute])]) + +const router = createRouter({ routeTree }) + +test('clicking tabs navigate to correct tab', () => { + // @ts-expect-error - typings + render(, { wrapper: Wrapper }) + const settingsButton = screen.getByText('Settings') + settingsButton.click() + const settingsRouteName = router.state.location.pathname + expect(settingsRouteName).toStrictEqual('/tab2') + + const observationTab = screen.getByTestId('tab-observation') + observationTab.click() + const observationTabRouteName = router.state.location.pathname + expect(observationTabRouteName).toStrictEqual('/tab1') + + const aboutTab = screen.getByText('About') + aboutTab.click() + const aboutTabRoute = router.state.location.pathname + expect(aboutTabRoute).toStrictEqual('/tab2') }) diff --git a/src/renderer/src/routes/(MapTabs)/_Map.tsx b/src/renderer/src/routes/(MapTabs)/_Map.tsx index 6acbacf..70826f1 100644 --- a/src/renderer/src/routes/(MapTabs)/_Map.tsx +++ b/src/renderer/src/routes/(MapTabs)/_Map.tsx @@ -1,49 +1,42 @@ -import * as React from 'react' -import { Paper } from '@mui/material' -import Tab from '@mui/material/Tab' -import Tabs from '@mui/material/Tabs' -import { Outlet, createFileRoute, useNavigate } from '@tanstack/react-router' +import { Suspense } from 'react' +import { CircularProgress, Paper } from '@mui/material' +import { styled } from '@mui/material/styles' +import { Outlet, createFileRoute } from '@tanstack/react-router' -import type { FileRoutesById } from '../../routeTree.gen' +import { VERY_LIGHT_GREY, WHITE } from '../../colors' +import { Tabs } from '../../components/Tabs' + +const Container = styled('div')({ + display: 'flex', + backgroundColor: WHITE, + height: '100%', +}) export const Route = createFileRoute('/(MapTabs)/_Map')({ component: MapLayout, }) export function MapLayout() { - const navigate = useNavigate() - const renderCount = React.useRef(0) - renderCount.current = renderCount.current + 1 return ( -
- navigate({ to: value as MapTabRoute })} - orientation="vertical" - > - - - - - + + + +
+ }> + + +
-
map component here
-
parent map component render count: {renderCount.current}
-
+ }> +
map component here
+
+ ) } - -type TabProps = React.ComponentProps - -type MapTabRoute = { - [K in keyof FileRoutesById]: K extends `${'/(MapTabs)/_Map'}${infer Rest}` - ? Rest extends '' - ? never - : `${Rest}` - : never -}[keyof FileRoutesById] - -type MapTabProps = Omit & { value: MapTabRoute } - -function MapTab(props: MapTabProps) { - return -} diff --git a/src/renderer/src/routes/__root.tsx b/src/renderer/src/routes/__root.tsx index 9946ccc..7f8173e 100644 --- a/src/renderer/src/routes/__root.tsx +++ b/src/renderer/src/routes/__root.tsx @@ -1,7 +1,8 @@ +import { Suspense } from 'react' import { CssBaseline, ThemeProvider } from '@mui/material' +import CircularProgress from '@mui/material/CircularProgress' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Outlet, createRootRoute } from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { theme } from '../Theme' import { ApiProvider } from '../contexts/ApiContext' @@ -16,8 +17,9 @@ export const Route = createRootRoute({ - - + }> + + diff --git a/src/renderer/src/routes/index.tsx b/src/renderer/src/routes/index.tsx index a06e285..5254a96 100644 --- a/src/renderer/src/routes/index.tsx +++ b/src/renderer/src/routes/index.tsx @@ -1,7 +1,5 @@ -import * as React from 'react' -import Box from '@mui/material/Box' -import CircularProgress from '@mui/material/CircularProgress' -import { createFileRoute, useRouter } from '@tanstack/react-router' +import { useLayoutEffect } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useDeviceInfo } from '../queries/deviceInfo' @@ -9,22 +7,21 @@ export const Route = createFileRoute('/')({ component: RouteComponent, }) +// the user will never get here, they will be redirected. +// While this code is semi hacky, the suggested alternative is to redirect in the (beforeLoad)[https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes#the-routebeforeload-option]. The problem is that this requires the router to use 'useDeviceInfo'. We could pass this hook to the router via the RouterContext. But i think the complexity of passing that info makes this hacky code slightly more desirable and easy to understand. + function RouteComponent() { - const router = useRouter() + const navigate = useNavigate() const { data } = useDeviceInfo() const hasCreatedDeviceName = data?.name !== undefined - React.useEffect(() => { + useLayoutEffect(() => { if (!hasCreatedDeviceName) { - router.navigate({ to: '/Onboarding' }) + navigate({ to: '/Onboarding' }) } else { - router.navigate({ to: '/tab1' }) + navigate({ to: '/tab1' }) } - }, [hasCreatedDeviceName]) + }) - return ( - - - - ) + return null } diff --git a/src/renderer/vite.config.js b/src/renderer/vite.config.js index 8c88d3a..0c6514f 100644 --- a/src/renderer/vite.config.js +++ b/src/renderer/vite.config.js @@ -1,3 +1,5 @@ +/// +/// import * as path from 'node:path' import { fileURLToPath } from 'node:url' import { TanStackRouterVite } from '@tanstack/router-plugin/vite'