router config changes

This commit is contained in:
yohlo
2025-08-26 22:47:25 -05:00
parent fcdb33a4b6
commit 75479be334
16 changed files with 87 additions and 101 deletions

View File

@@ -3,7 +3,7 @@ import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routerWithQueryClient } from '@tanstack/react-router-with-query' import { routerWithQueryClient } from '@tanstack/react-router-with-query'
import { routeTree } from './routeTree.gen' import { routeTree } from './routeTree.gen'
import { DefaultCatchBoundary } from '../components/DefaultCatchBoundary' import { DefaultCatchBoundary } from '../components/DefaultCatchBoundary'
import { defaultHeaderConfig } from '@/features/core/hooks/use-header-config' import { defaultHeaderConfig } from '@/features/core/hooks/use-router-config'
export function createRouter() { export function createRouter() {
const queryClient = new QueryClient({ const queryClient = new QueryClient({
@@ -21,7 +21,7 @@ export function createRouter() {
return routerWithQueryClient( return routerWithQueryClient(
createTanStackRouter({ createTanStackRouter({
routeTree, routeTree,
context: { queryClient, auth: undefined!, header: defaultHeaderConfig, refresh: { toRefresh: [] } }, context: { queryClient, auth: undefined!, header: defaultHeaderConfig, refresh: [], withPadding: true },
defaultPreload: 'intent', defaultPreload: 'intent',
defaultErrorComponent: DefaultCatchBoundary, defaultErrorComponent: DefaultCatchBoundary,
scrollRestoration: true, scrollRestoration: true,

View File

@@ -21,7 +21,8 @@ export const Route = createRootRouteWithContext<{
queryClient: QueryClient, queryClient: QueryClient,
auth: AuthContextType, auth: AuthContextType,
header: HeaderConfig, header: HeaderConfig,
refresh: { toRefresh: string[] } refresh: string[]
withPadding: boolean
}>()({ }>()({
head: () => ({ head: () => ({
meta: [ meta: [

View File

@@ -13,15 +13,12 @@ export const Route = createFileRoute("/_authed/admin/tournaments/")({
withBackButton: true, withBackButton: true,
title: "Manage Tournaments", title: "Manage Tournaments",
}, },
refresh: { refresh: tournamentQueries.list().queryKey,
toRefresh: tournamentQueries.list().queryKey, withPadding: false
},
}), }),
component: RouteComponent, component: RouteComponent,
}); });
function RouteComponent() { function RouteComponent() {
return <Page noPadding> return <ManageTournaments />
<ManageTournaments />
</Page>
} }

View File

@@ -10,11 +10,14 @@ export const Route = createFileRoute("/_authed/")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
await context.queryClient.ensureQueryData(tournamentQueries.list()); await context.queryClient.ensureQueryData(tournamentQueries.list());
}, },
loader: () => ({
withPadding: false
})
}); });
function Home() { function Home() {
return ( return (
<Page noPadding> <>
<Box h='60vh' p="md"> <Box h='60vh' p="md">
<Text m='16vh' fw={500}>Some Content Here</Text> <Text m='16vh' fw={500}>Some Content Here</Text>
</Box> </Box>
@@ -24,6 +27,6 @@ function Home() {
<Divider /> <Divider />
<ListLink label="All Tournaments" to="/tournaments" Icon={TrophyIcon} /> <ListLink label="All Tournaments" to="/tournaments" Icon={TrophyIcon} />
</Box> </Box>
</Page> </>
); );
} }

View File

@@ -24,12 +24,10 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
withBackButton: true, withBackButton: true,
settingsLink: context?.auth.user.id === params.playerId ? 'settings' : undefined settingsLink: context?.auth.user.id === params.playerId ? 'settings' : undefined
}, },
refresh: { refresh: [playerQueries.details(params.playerId).queryKey]
toRefresh: [playerQueries.details(params.playerId).queryKey],
}
}), }),
component: () => { component: () => {
const { player } = Route.useRouteContext(); const { player } = Route.useRouteContext();
return <Page><Profile player={player} /></Page> return <Profile player={player} />
}, },
}) })

View File

@@ -4,7 +4,6 @@ import { ColorSchemePicker } from "@/features/settings/components/color-scheme-p
import AccentColorPicker from "@/features/settings/components/accent-color-picker" import AccentColorPicker from "@/features/settings/components/accent-color-picker"
import { SignOutIcon } from "@phosphor-icons/react" import { SignOutIcon } from "@phosphor-icons/react"
import ListLink from "@/components/list-link" import ListLink from "@/components/list-link"
import Page from "@/components/page"
export const Route = createFileRoute("/_authed/settings")({ export const Route = createFileRoute("/_authed/settings")({
loader: () => ({ loader: () => ({
@@ -12,12 +11,13 @@ export const Route = createFileRoute("/_authed/settings")({
title: 'Settings', title: 'Settings',
withBackButton: true, withBackButton: true,
}, },
withPadding: false
}), }),
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { function RouteComponent() {
return <Page noPadding> return <>
<Box px='md' py='sm' style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}> <Box px='md' py='sm' style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
<Title order={3}>Appearance</Title> <Title order={3}>Appearance</Title>
<Stack> <Stack>
@@ -30,5 +30,5 @@ function RouteComponent() {
to='/logout' to='/logout'
Icon={SignOutIcon} Icon={SignOutIcon}
/> />
</Page> </>
} }

View File

@@ -21,12 +21,10 @@ export const Route = createFileRoute("/_authed/teams/$teamId")({
collapsed: true, collapsed: true,
withBackButton: true withBackButton: true
}, },
refresh: { refresh: [teamQueries.details(params.teamId).queryKey]
toRefresh: [teamQueries.details(params.teamId).queryKey],
}
}), }),
component: () => { component: () => {
const { team } = Route.useRouteContext(); const { team } = Route.useRouteContext();
return <Page><TeamProfile team={team} /></Page> return <TeamProfile team={team} />
}, },
}) })

View File

@@ -28,16 +28,13 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
withBackButton: true, withBackButton: true,
settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined
}, },
refresh: { refresh: tournamentQueries.details(params.tournamentId).queryKey,
toRefresh: tournamentQueries.details(params.tournamentId).queryKey, withPadding: false
}
}), }),
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { function RouteComponent() {
const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId)); const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId));
return <Page noPadding> return <Profile tournament={tournament!} />
<Profile tournament={tournament!} />
</Page>
} }

View File

@@ -21,9 +21,7 @@ export const Route = createFileRoute('/_authed/tournaments/')({
withBackButton: true, withBackButton: true,
title: 'Tournaments', title: 'Tournaments',
}, },
refresh: { refresh: tournamentQueries.list().queryKey
toRefresh: tournamentQueries.list().queryKey,
}
}), }),
component: RouteComponent, component: RouteComponent,
}) })

View File

@@ -1,13 +1,13 @@
import { Container, ContainerProps } from "@mantine/core"; import { Container, ContainerProps } from "@mantine/core";
import useHeaderConfig from "@/features/core/hooks/use-header-config"; import useRouterConfig from "@/features/core/hooks/use-router-config";
interface PageProps extends ContainerProps, React.PropsWithChildren { interface PageProps extends ContainerProps, React.PropsWithChildren {
noPadding?: boolean; noPadding?: boolean;
} }
const Page = ({ children, noPadding, ...props }: PageProps) => { const Page = ({ children, noPadding, ...props }: PageProps) => {
const headerConfig = useHeaderConfig(); const { header } = useRouterConfig();
return <Container px={noPadding ? 0 : 'md'} pt={headerConfig.collapsed ? 60 : 0} pb={20} m={0} maw={600} mx='auto' {...props}> return <Container px={noPadding ? 0 : 'md'} pt={header.collapsed ? 60 : 0} pb={20} m={0} maw={600} mx='auto' {...props}>
{children} {children}
</Container> </Container>
} }

View File

@@ -2,20 +2,22 @@ import { AppShell } from '@mantine/core';
import { PropsWithChildren, useState } from 'react'; import { PropsWithChildren, useState } from 'react';
import Header from './header'; import Header from './header';
import Navbar from './navbar'; import Navbar from './navbar';
import useHeaderConfig from '../hooks/use-header-config';
import Pullable from './pullable'; import Pullable from './pullable';
import useVisualViewportSize from '../hooks/use-visual-viewport-size'; import useVisualViewportSize from '../hooks/use-visual-viewport-size';
import useRouterConfig from '../hooks/use-router-config';
import Page from '@/components/page';
const Layout: React.FC<PropsWithChildren> = ({ children }) => { const Layout: React.FC<PropsWithChildren> = ({ children }) => {
const headerConfig = useHeaderConfig(); const { header } = useRouterConfig();
const viewport = useVisualViewportSize(); const viewport = useVisualViewportSize();
const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 }); const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });
const { withPadding } = useRouterConfig();
return ( return (
<AppShell <AppShell
id='app-shell' id='app-shell'
layout='alt' layout='alt'
header={{ height: 60, collapsed: headerConfig.collapsed }} header={{ height: 60, collapsed: header.collapsed }}
navbar={{ navbar={{
width: { base: 0, sm: 100, md: 200, lg: 300 }, width: { base: 0, sm: 100, md: 200, lg: 300 },
breakpoint: 'sm', breakpoint: 'sm',
@@ -31,7 +33,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
mah='100dvh' mah='100dvh'
style={{ top: viewport.top }} //, transition: 'top 0.1s ease-in-out' }} style={{ top: viewport.top }} //, transition: 'top 0.1s ease-in-out' }}
> >
<Header scrollPosition={scrollPosition} {...headerConfig} /> <Header scrollPosition={scrollPosition} {...header} />
<AppShell.Main <AppShell.Main
pos='relative' pos='relative'
h='100%' h='100%'
@@ -41,7 +43,9 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
style={{ transition: 'none' }} style={{ transition: 'none' }}
> >
<Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}> <Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}>
<Page noPadding={!withPadding}>
{children} {children}
</Page>
</Pullable> </Pullable>
</AppShell.Main> </AppShell.Main>
<Navbar /> <Navbar />

View File

@@ -1,9 +1,9 @@
import { ActionIcon, Box, Button, Flex, ScrollArea } from "@mantine/core"; import { ActionIcon, Box, Button, Flex, ScrollArea } from "@mantine/core";
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react";
import useAppShellHeight from "@/hooks/use-appshell-height"; import useAppShellHeight from "@/hooks/use-appshell-height";
import useRefreshConfig from "@/features/core/hooks/use-refresh-config";
import { ArrowClockwiseIcon, SpinnerIcon } from "@phosphor-icons/react"; import { ArrowClockwiseIcon, SpinnerIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import useRouterConfig from "../hooks/use-router-config";
const THRESHOLD = 80; const THRESHOLD = 80;
@@ -20,20 +20,20 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
const height = useAppShellHeight(); const height = useAppShellHeight();
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [scrolling, setScrolling] = useState(false); const [scrolling, setScrolling] = useState(false);
const { toRefresh } = useRefreshConfig(); const { refresh } = useRouterConfig();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const scrollY = useMemo(() => scrollPosition.y < 0 && scrolling ? Math.abs(scrollPosition.y) : 0, [scrollPosition.y, scrolling]); const scrollY = useMemo(() => scrollPosition.y < 0 && scrolling ? Math.abs(scrollPosition.y) : 0, [scrollPosition.y, scrolling]);
const onTrigger = useCallback(async () => { const onTrigger = useCallback(async () => {
setIsRefreshing(true); setIsRefreshing(true);
if (toRefresh.length > 0) { if (refresh.length > 0) {
// TODO: Remove this after testing - or does the delay help ux? // TODO: Remove this after testing - or does the delay help ux?
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
await queryClient.refetchQueries({ queryKey: toRefresh, exact: true}); await queryClient.refetchQueries({ queryKey: refresh, exact: true});
} }
setIsRefreshing(false); setIsRefreshing(false);
}, [toRefresh]); }, [refresh]);
useEffect(() => { useEffect(() => {
if (!isRefreshing && scrollY > THRESHOLD) { if (!isRefreshing && scrollY > THRESHOLD) {
@@ -43,7 +43,7 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
const iconOpacity = useMemo(() => { const iconOpacity = useMemo(() => {
if (isRefreshing) return 1; if (isRefreshing) return 1;
if (toRefresh.length === 0) return 0; if (refresh.length === 0) return 0;
const clampedValue = Math.max(5, Math.min(THRESHOLD, scrollY)); const clampedValue = Math.max(5, Math.min(THRESHOLD, scrollY));
const min = 5; const min = 5;
@@ -111,7 +111,7 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
> >
<Box pt='1rem'pb='0.285rem' mih={height} style={{ boxSizing: 'content-box' }}> <Box pt='1rem'pb='0.285rem' mih={height} style={{ boxSizing: 'content-box' }}>
{ /* TODO: Remove this debug button */} { /* TODO: Remove this debug button */}
<ActionIcon style={{ zIndex: 1000 }} pos='absolute' top={8} left='calc(50% - 24px)' onClick={onTrigger} variant='filled' color='var(--mantine-color-dimmed)'> <ActionIcon display={!!refresh.length ? 'unset' : 'none' } style={{ zIndex: 1000 }} pos='absolute' top={8} left='calc(50% - 24px)' onClick={onTrigger} variant='filled' color='var(--mantine-color-dimmed)'>
<ArrowClockwiseIcon /> <ArrowClockwiseIcon />
</ActionIcon> </ActionIcon>
{children} {children}

View File

@@ -1,27 +0,0 @@
import { isMatch, useMatches } from "@tanstack/react-router";
import { HeaderConfig } from "../types/header-config";
export const defaultHeaderConfig: HeaderConfig = {
title: 'FLXN',
withBackButton: false,
collapsed: false,
}
const useHeaderConfig = () => {
const matches = useMatches();
const matchesWithHeader = matches.filter((match) =>
isMatch(match, 'loaderData.header'),
)
const config = matchesWithHeader.reduce((acc, match) => {
return {
...acc,
...match?.loaderData?.header,
}
}, defaultHeaderConfig) as HeaderConfig;
return config;
}
export default useHeaderConfig;

View File

@@ -1,24 +0,0 @@
import { isMatch, useMatches } from "@tanstack/react-router";
export const defaultRefreshConfig: { toRefresh: string[] } = {
toRefresh: [],
}
const useRefreshConfig = () => {
const matches = useMatches();
const matchesWithRefresh = matches.filter((match) =>
isMatch(match, 'loaderData.refresh'),
)
const config = matchesWithRefresh.reduce((acc, match) => {
return {
...acc,
...match?.loaderData?.refresh,
}
}, defaultRefreshConfig) as { toRefresh: string[] };
return config;
}
export default useRefreshConfig;

View File

@@ -0,0 +1,40 @@
import { useMatches } from "@tanstack/react-router";
import { HeaderConfig } from "../types/header-config";
export const defaultHeaderConfig: HeaderConfig = {
title: 'FLXN',
withBackButton: false,
collapsed: false,
}
const useRouterConfig = () => {
const matches = useMatches();
const matchesWithHeader = matches.filter((match) =>
match?.loaderData && 'header' in match.loaderData
);
const headerConfig = matchesWithHeader.reduce((acc, match) => {
const loaderData = match?.loaderData;
if (loaderData && typeof loaderData === 'object' && 'header' in loaderData) {
const header = loaderData.header;
if (header && typeof header === 'object') {
return {
...acc,
...header,
}
}
}
return acc;
}, defaultHeaderConfig);
const current = matches[matches.length - 1]?.loaderData;
return {
header: headerConfig,
refresh: current && typeof current === 'object' && 'refresh' in current ? current.refresh : [],
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true
};
}
export default useRouterConfig;

View File

@@ -1,18 +1,19 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useIsMobile } from "./use-is-mobile"; import { useIsMobile } from "./use-is-mobile";
import useHeaderConfig from "@/features/core/hooks/use-header-config"; import useRouterConfig from "@/features/core/hooks/use-router-config";
const useAppShellHeight = () => { const useAppShellHeight = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const headerConfig = useHeaderConfig(); const { header } = useRouterConfig();
const height = useMemo(() => { const height = useMemo(() => {
const appShellBottomPadding = isMobile ? '70px' : '0px'; const appShellBottomPadding = isMobile ? '70px' : '0px';
const pageBottomPadding = '20px'; const pageBottomPadding = '20px';
const mobileNavbar = isMobile && !headerConfig.collapsed ? '4rem' : '0px'; const mobileNavbar = isMobile && !header.collapsed ? '4rem' : '0px';
const pullablePadding = '1.285rem'; const pullablePadding = '1.285rem';
return `calc(100dvh - var(--app-shell-header-height, 0px) - ${mobileNavbar} - ${pullablePadding} - ${appShellBottomPadding} - ${pageBottomPadding})`; return `calc(100dvh - var(--app-shell-header-height, 0px) - ${mobileNavbar} - ${pullablePadding} - ${appShellBottomPadding} - ${pageBottomPadding})`;
}, [isMobile, headerConfig.collapsed]); }, [isMobile, header.collapsed]);
return height; return height;
} }