diff --git a/index.html b/index.html index 1899d8a98..bd3de5cf4 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ KittyCAD Modeling App - +
diff --git a/src/Auth.tsx b/src/Auth.tsx index aeb34e8ff..090d8ff5d 100644 --- a/src/Auth.tsx +++ b/src/Auth.tsx @@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom' import { useEffect } from 'react' import { isTauri } from './lib/isTauri' import Loading from './components/Loading' +import { paths } from './Router' // Wrapper around protected routes, used in src/Router.tsx export const Auth = ({ children }: React.PropsWithChildren) => { @@ -27,7 +28,7 @@ export const Auth = ({ children }: React.PropsWithChildren) => { (isTauri() && !token) || (!isTauri() && !isLoading && !(user && 'id' in user)) ) { - navigate('/signin') + navigate(paths.SIGN_IN) } }, [user, token, navigate, isLoading]) diff --git a/src/Router.tsx b/src/Router.tsx index 6f10de711..4fe4a5851 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,52 +1,84 @@ import { App } from './App' -import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom' +import { + createBrowserRouter, + Outlet, + redirect, + RouterProvider, +} from 'react-router-dom' import { ErrorPage } from './components/ErrorPage' import { Settings } from './routes/Settings' -import Onboarding, { onboardingRoutes } from './routes/Onboarding' +import Onboarding, { + onboardingRoutes, + onboardingPaths, +} from './routes/Onboarding' import SignIn from './routes/SignIn' import { Auth } from './Auth' +const prependRoutes = + (routesObject: Record) => (prepend: string) => { + return Object.fromEntries( + Object.entries(routesObject).map(([constName, path]) => [ + constName, + prepend + path, + ]) + ) + } + +export const paths = { + INDEX: '/', + SETTINGS: '/settings', + SIGN_IN: '/signin', + ONBOARDING: prependRoutes(onboardingPaths)( + '/onboarding/' + ) as typeof onboardingPaths, +} + const router = createBrowserRouter([ { - path: '/', + path: paths.INDEX, element: ( + ), errorElement: , - loader: () => { + loader: ({ request }) => { const store = localStorage.getItem('store') if (store === null) { - return redirect('/onboarding') + return redirect(paths.ONBOARDING.INDEX) } else { - const status = JSON.parse(store).state.onboardingStatus - if (status !== 'done' && status !== 'dismissed') { - return redirect('/onboarding/' + status) + const status = JSON.parse(store).state.onboardingStatus || '' + const notEnRouteToOnboarding = + !request.url.includes(paths.ONBOARDING.INDEX) && + request.method === 'GET' + // '' is the initial state, 'done' and 'dismissed' are the final states + const hasValidOnboardingStatus = + (status !== undefined && status.length === 0) || + !(status === 'done' || status === 'dismissed') + const shouldRedirectToOnboarding = + notEnRouteToOnboarding && hasValidOnboardingStatus + + if (shouldRedirectToOnboarding) { + return redirect(paths.ONBOARDING.INDEX + status) } } return null }, + children: [ + { + path: paths.SETTINGS, + element: , + }, + { + path: paths.ONBOARDING.INDEX, + element: , + children: onboardingRoutes, + }, + ], }, { - path: '/settings', - element: ( - - - - ), - }, - { - path: '/onboarding', - element: ( - - - - ), - children: onboardingRoutes, - }, - { - path: '/signin', + path: paths.SIGN_IN, element: , }, ]) diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index 6828b944f..e1f0f8eb2 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom' import { ActionIcon, ActionIconProps } from './ActionIcon' import React from 'react' +import { paths } from '../Router' interface ActionButtonProps extends React.PropsWithChildren { icon?: ActionIconProps @@ -17,7 +18,7 @@ export const ActionButton = ({ icon, className, onClick, - to = '/', + to = paths.INDEX, Element = 'button', children, ...props diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index cf86432bf..f1174cf01 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -2,6 +2,7 @@ import { Link } from 'react-router-dom' import { Toolbar } from '../Toolbar' import { useStore } from '../useStore' import UserSidebarMenu from './UserSidebarMenu' +import { paths } from '../Router' interface AppHeaderProps extends React.PropsWithChildren { showToolbar?: boolean @@ -24,7 +25,7 @@ export const AppHeader = ({ className } > - + KittyCAD App { const displayedName = getDisplayName(user) @@ -58,61 +59,72 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { - {user && ( -
- {user.image && !imageLoadFailed && ( -
- {user.name setImageLoadFailed(true)} - /> + {({ close }) => ( + <> + {user && ( +
+ {user.image && !imageLoadFailed && ( +
+ {user.name setImageLoadFailed(true)} + /> +
+ )} + +
+

+ {displayedName || ''} +

+ {displayedName !== user.email && ( +

+ {user.email} +

+ )} +
)} - -
-

+ { + // since /settings is a nested route the sidebar doesn't close + // automatically when navigating to it + close() + navigate(paths.SETTINGS) + }} > - {displayedName || ''} -

- {displayedName !== user.email && ( -

- {user.email} -

- )} + Settings + + { + setToken('') + navigate(paths.SIGN_IN) + }} + icon={{ + icon: faSignOutAlt, + bgClassName: 'bg-destroy-80', + iconClassName: + 'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10', + }} + className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60" + > + Sign out +
-
+ )} -
- - Settings - - { - setToken('') - navigate('/signin') - }} - icon={{ - icon: faSignOutAlt, - bgClassName: 'bg-destroy-80', - iconClassName: - 'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10', - }} - className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60" - > - Sign out - -
) diff --git a/src/index.css b/src/index.css index db9ea24c0..f2f8b3ec9 100644 --- a/src/index.css +++ b/src/index.css @@ -11,15 +11,24 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - @apply text-chalkboard-110 bg-chalkboard-10; + @apply text-chalkboard-110; overflow: hidden; scrollbar-width: thin; scrollbar-color: var(--color-chalkboard-20) var(--color-chalkboard-40); } +.body-bg { + @apply bg-chalkboard-10; +} + +.body-bg.dark, +.dark .body-bg { + @apply bg-chalkboard-100; +} + body.dark { scrollbar-color: var(--color-chalkboard-70) var(--color-chalkboard-90); - @apply bg-chalkboard-100 text-chalkboard-10; + @apply text-chalkboard-10; } ::-webkit-scrollbar { diff --git a/src/routes/Onboarding/index.tsx b/src/routes/Onboarding/index.tsx index 131707006..fa5f73a37 100644 --- a/src/routes/Onboarding/index.tsx +++ b/src/routes/Onboarding/index.tsx @@ -1,29 +1,36 @@ import { useHotkeys } from 'react-hotkeys-hook' import { Outlet, useNavigate } from 'react-router-dom' import { useStore } from '../../useStore' -import { App } from '../../App' import Introduction from './Introduction' import Units from './Units' import Camera from './Camera' import Sketching from './Sketching' import { useCallback } from 'react' +import { paths } from '../../Router' + +export const onboardingPaths = { + INDEX: '', + UNITS: 'units', + CAMERA: 'camera', + SKETCHING: 'sketching', +} export const onboardingRoutes = [ { - path: '', + index: true, element: , }, { - path: 'units', + path: onboardingPaths.UNITS, element: , }, { - path: 'camera', + path: onboardingPaths.CAMERA, element: , }, { - path: 'sketching', + path: onboardingPaths.SKETCHING, element: , }, ] @@ -48,7 +55,7 @@ export function useDismiss() { return useCallback(() => { setOnboardingStatus('dismissed') - navigate('/') + navigate(paths.INDEX) }, [setOnboardingStatus, navigate]) } @@ -59,7 +66,6 @@ const Onboarding = () => { return ( <> - ) } diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index b6158b5cd..3d7c91b1a 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -12,10 +12,11 @@ import { toast } from 'react-hot-toast' import { Toggle } from '../components/Toggle/Toggle' import { useNavigate } from 'react-router-dom' import { useHotkeys } from 'react-hotkeys-hook' +import { paths } from '../Router' export const Settings = () => { const navigate = useNavigate() - useHotkeys('esc', () => navigate('/')) + useHotkeys('esc', () => navigate(paths.INDEX)) const { defaultDir, setDefaultDir, @@ -50,7 +51,7 @@ export const Settings = () => { async function handleDirectorySelection() { const newDirectory = await open({ directory: true, - defaultPath: (defaultDir.base || '') + (defaultDir.dir || '/'), + defaultPath: (defaultDir.base || '') + (defaultDir.dir || paths.INDEX), title: 'Choose a new default directory', }) @@ -60,11 +61,11 @@ export const Settings = () => { } return ( - <> +
{ { setOnboardingStatus('') - navigate('/') + navigate(paths.ONBOARDING.INDEX) }} icon={{ icon: faArrowRotateBack }} > @@ -219,7 +220,7 @@ export const Settings = () => {
- +
) } diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index 4aa31acfd..289ecc60c 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -6,6 +6,7 @@ import { invoke } from '@tauri-apps/api/tauri' import { useNavigate } from 'react-router-dom' import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env' import { getSystemTheme } from '../lib/getSystemTheme' +import { paths } from '../Router' const SignIn = () => { const navigate = useNavigate() @@ -21,7 +22,7 @@ const SignIn = () => { host: VITE_KC_API_BASE_URL, }) setToken(token) - navigate('/') + navigate(paths.INDEX) } catch (error) { console.error('login button', error) } @@ -69,7 +70,9 @@ const SignIn = () => { ) : (