Skip to content
← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

FollowFork

starter-tanstack/app/routeTree.tsx

165 lines · 4.9 KB · TypeScript
import {
	Outlet,
	createRootRouteWithContext,
	createRoute,
	createRouter,
	useRouter,
	useRouterState,
} from '@tanstack/react-router'
import type { RouterHistory } from '@tanstack/react-router'
import type { AppShellContext } from './router-context.ts'
import { AppShellReactContext } from './router-context.ts'
import { Document } from './ui/document.tsx'
import { ErrorBanner } from './ui/error-banner.tsx'
import { HomePage } from './pages/home.tsx'
import { AboutPage } from './pages/about.tsx'
import { NotesPage } from './pages/notes.tsx'
import { toErrorMessage } from './ui/to-error-message.ts'
import type { Note } from './data/notes.ts'

export type RouterContext = AppShellContext

type PageStaticData = { page?: string; title?: string }

const rootRoute = createRootRouteWithContext<RouterContext>()({
	component: function Root() {
		const router = useRouter()
		const ctx = router.options.context as RouterContext
		const leaf = useRouterState({ select: (s) => s.matches.at(-1) })
		const staticData = (leaf?.staticData || {}) as PageStaticData
		return (
			<AppShellReactContext.Provider value={ctx}>
				<Document
					title={staticData.title || '__APP_TITLE__'}
					page={staticData.page || ''}
					runningSha={ctx.runningSha}
					appBasePath={ctx.appBasePath}
					assetBasePath={ctx.assetBasePath}
					clientModuleUrl={ctx.clientModuleUrl}
				>
					<Outlet />
				</Document>
			</AppShellReactContext.Provider>
		)
	},
	errorComponent: function RootError({ error }) {
		const router = useRouter()
		const ctx = router.options.context as RouterContext
		return (
			<AppShellReactContext.Provider value={ctx}>
				<Document
					title="Error · __APP_TITLE__"
					appBasePath={ctx.appBasePath}
					assetBasePath={ctx.assetBasePath}
					clientModuleUrl={ctx.clientModuleUrl}
					runningSha={ctx.runningSha}
				>
					<div className="pak-wrap pak-stack">
						<ErrorBanner
							title="Something went wrong"
							message={toErrorMessage(error)}
							detail={toErrorMessage(error)}
						/>
					</div>
				</Document>
			</AppShellReactContext.Provider>
		)
	},
})

const homeRoute = createRoute({
	getParentRoute: () => rootRoute,
	path: '/',
	staticData: { page: 'home', title: '__APP_TITLE__' } satisfies PageStaticData,
	loader: async ({ context }): Promise<{ noteCount: number }> => {
		if (context.listNotes) {
			const notes = await context.listNotes()
			return { noteCount: notes.length }
		}
		const appBase = context.appBasePath.replace(/\/$/, '')
		const res = await fetch(`${appBase}/notes`, {
			headers: { Accept: 'application/json' },
			credentials: 'same-origin',
		})
		if (!res.ok) return { noteCount: 0 }
		const data = (await res.json()) as { notes?: Array<Note> }
		return { noteCount: Array.isArray(data.notes) ? data.notes.length : 0 }
	},
	component: HomePage,
})

const aboutRoute = createRoute({
	getParentRoute: () => rootRoute,
	path: '/about',
	staticData: { page: 'about', title: 'About · __APP_TITLE__' } satisfies PageStaticData,
	loader: async ({ context }) => {
		if (context.loadAbout) return await context.loadAbout()
		const appBase = context.appBasePath.replace(/\/$/, '')
		const res = await fetch(`${appBase}/api/version`, {
			cache: 'no-store',
			credentials: 'same-origin',
		})
		const version = res.ok
			? ((await res.json()) as {
					sha?: string
					shortSha?: string
					message?: string
					publishedAt?: string
					committedAt?: string
				})
			: {}
		return {
			sha: version.sha || '',
			shortSha: version.shortSha || '',
			message: version.message || '',
			publishedAt: version.publishedAt || '',
			committedAt: version.committedAt || '',
			codeUrl: null as string | null,
			runningSha: version.sha || '',
		}
	},
	component: AboutPage,
})

const notesRoute = createRoute({
	getParentRoute: () => rootRoute,
	path: '/notes',
	staticData: { page: 'notes', title: 'Notes · __APP_TITLE__' } satisfies PageStaticData,
	loader: async ({ context }): Promise<{ notes: Array<Note> }> => {
		if (context.listNotes) {
			return { notes: await context.listNotes() }
		}
		const appBase = context.appBasePath.replace(/\/$/, '')
		const res = await fetch(`${appBase}/notes`, {
			headers: { Accept: 'application/json' },
			credentials: 'same-origin',
		})
		if (!res.ok) return { notes: [] }
		const data = (await res.json()) as { notes?: Array<Note> }
		return { notes: Array.isArray(data.notes) ? data.notes : [] }
	},
	component: NotesPage,
})

export const routeTree = rootRoute.addChildren([homeRoute, aboutRoute, notesRoute])

export function createAppRouter(input: {
	history: RouterHistory
	context: RouterContext
	basepath?: string
}) {
	return createRouter({
		routeTree,
		history: input.history,
		context: input.context,
		basepath: input.basepath && input.basepath !== '/' ? input.basepath : undefined,
		defaultPreload: 'intent',
		scrollRestoration: true,
	})
}

declare module '@tanstack/react-router' {
	interface Register {
		router: ReturnType<typeof createAppRouter>
	}
}