diff --git a/src/app/router.tsx b/src/app/router.tsx index a3d9839..3bd66d5 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -3,7 +3,7 @@ import { createRouter as createTanStackRouter } from '@tanstack/react-router' import { routerWithQueryClient } from '@tanstack/react-router-with-query' import { routeTree } from './routeTree.gen' 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() { const queryClient = new QueryClient({ @@ -21,7 +21,7 @@ export function createRouter() { return routerWithQueryClient( createTanStackRouter({ routeTree, - context: { queryClient, auth: undefined!, header: defaultHeaderConfig, refresh: { toRefresh: [] } }, + context: { queryClient, auth: undefined!, header: defaultHeaderConfig, refresh: [], withPadding: true }, defaultPreload: 'intent', defaultErrorComponent: DefaultCatchBoundary, scrollRestoration: true, diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index dac2d58..9c232ad 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -21,7 +21,8 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient, auth: AuthContextType, header: HeaderConfig, - refresh: { toRefresh: string[] } + refresh: string[] + withPadding: boolean }>()({ head: () => ({ meta: [ diff --git a/src/app/routes/_authed/admin/tournaments/index.tsx b/src/app/routes/_authed/admin/tournaments/index.tsx index 92a928a..8b9ab86 100644 --- a/src/app/routes/_authed/admin/tournaments/index.tsx +++ b/src/app/routes/_authed/admin/tournaments/index.tsx @@ -13,15 +13,12 @@ export const Route = createFileRoute("/_authed/admin/tournaments/")({ withBackButton: true, title: "Manage Tournaments", }, - refresh: { - toRefresh: tournamentQueries.list().queryKey, - }, + refresh: tournamentQueries.list().queryKey, + withPadding: false }), component: RouteComponent, }); function RouteComponent() { - return - - + return } diff --git a/src/app/routes/_authed/index.tsx b/src/app/routes/_authed/index.tsx index 8c62a38..7c0f17a 100644 --- a/src/app/routes/_authed/index.tsx +++ b/src/app/routes/_authed/index.tsx @@ -10,11 +10,14 @@ export const Route = createFileRoute("/_authed/")({ beforeLoad: async ({ context }) => { await context.queryClient.ensureQueryData(tournamentQueries.list()); }, + loader: () => ({ + withPadding: false + }) }); function Home() { return ( - + <> Some Content Here @@ -24,6 +27,6 @@ function Home() { - + ); } diff --git a/src/app/routes/_authed/profile.$playerId.tsx b/src/app/routes/_authed/profile.$playerId.tsx index af422d2..5ebfd2e 100644 --- a/src/app/routes/_authed/profile.$playerId.tsx +++ b/src/app/routes/_authed/profile.$playerId.tsx @@ -24,12 +24,10 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({ withBackButton: true, settingsLink: context?.auth.user.id === params.playerId ? 'settings' : undefined }, - refresh: { - toRefresh: [playerQueries.details(params.playerId).queryKey], - } + refresh: [playerQueries.details(params.playerId).queryKey] }), component: () => { const { player } = Route.useRouteContext(); - return + return }, }) diff --git a/src/app/routes/_authed/settings.tsx b/src/app/routes/_authed/settings.tsx index 052699f..71e09fc 100644 --- a/src/app/routes/_authed/settings.tsx +++ b/src/app/routes/_authed/settings.tsx @@ -4,7 +4,6 @@ import { ColorSchemePicker } from "@/features/settings/components/color-scheme-p import AccentColorPicker from "@/features/settings/components/accent-color-picker" import { SignOutIcon } from "@phosphor-icons/react" import ListLink from "@/components/list-link" -import Page from "@/components/page" export const Route = createFileRoute("/_authed/settings")({ loader: () => ({ @@ -12,12 +11,13 @@ export const Route = createFileRoute("/_authed/settings")({ title: 'Settings', withBackButton: true, }, + withPadding: false }), component: RouteComponent, }) function RouteComponent() { - return + return <> Appearance @@ -30,5 +30,5 @@ function RouteComponent() { to='/logout' Icon={SignOutIcon} /> - + } diff --git a/src/app/routes/_authed/teams.$teamId.tsx b/src/app/routes/_authed/teams.$teamId.tsx index 0ab150b..7095cd9 100644 --- a/src/app/routes/_authed/teams.$teamId.tsx +++ b/src/app/routes/_authed/teams.$teamId.tsx @@ -21,12 +21,10 @@ export const Route = createFileRoute("/_authed/teams/$teamId")({ collapsed: true, withBackButton: true }, - refresh: { - toRefresh: [teamQueries.details(params.teamId).queryKey], - } + refresh: [teamQueries.details(params.teamId).queryKey] }), component: () => { const { team } = Route.useRouteContext(); - return + return }, }) diff --git a/src/app/routes/_authed/tournaments/$tournamentId.tsx b/src/app/routes/_authed/tournaments/$tournamentId.tsx index c36f592..16e30bc 100644 --- a/src/app/routes/_authed/tournaments/$tournamentId.tsx +++ b/src/app/routes/_authed/tournaments/$tournamentId.tsx @@ -28,16 +28,13 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({ withBackButton: true, settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined }, - refresh: { - toRefresh: tournamentQueries.details(params.tournamentId).queryKey, - } + refresh: tournamentQueries.details(params.tournamentId).queryKey, + withPadding: false }), component: RouteComponent, }) function RouteComponent() { const { data: tournament } = useQuery(tournamentQueries.details(Route.useParams().tournamentId)); - return - - + return } diff --git a/src/app/routes/_authed/tournaments/index.tsx b/src/app/routes/_authed/tournaments/index.tsx index b5df4ed..42379d3 100644 --- a/src/app/routes/_authed/tournaments/index.tsx +++ b/src/app/routes/_authed/tournaments/index.tsx @@ -21,9 +21,7 @@ export const Route = createFileRoute('/_authed/tournaments/')({ withBackButton: true, title: 'Tournaments', }, - refresh: { - toRefresh: tournamentQueries.list().queryKey, - } + refresh: tournamentQueries.list().queryKey }), component: RouteComponent, }) diff --git a/src/components/page.tsx b/src/components/page.tsx index cdf1f8a..6d4382f 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -1,13 +1,13 @@ 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 { noPadding?: boolean; } const Page = ({ children, noPadding, ...props }: PageProps) => { - const headerConfig = useHeaderConfig(); - return + const { header } = useRouterConfig(); + return {children} } diff --git a/src/features/core/components/layout.tsx b/src/features/core/components/layout.tsx index b523e35..0a6640d 100644 --- a/src/features/core/components/layout.tsx +++ b/src/features/core/components/layout.tsx @@ -2,20 +2,22 @@ import { AppShell } from '@mantine/core'; import { PropsWithChildren, useState } from 'react'; import Header from './header'; import Navbar from './navbar'; -import useHeaderConfig from '../hooks/use-header-config'; import Pullable from './pullable'; import useVisualViewportSize from '../hooks/use-visual-viewport-size'; +import useRouterConfig from '../hooks/use-router-config'; +import Page from '@/components/page'; const Layout: React.FC = ({ children }) => { - const headerConfig = useHeaderConfig(); + const { header } = useRouterConfig(); const viewport = useVisualViewportSize(); const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 }); + const { withPadding } = useRouterConfig(); return ( = ({ children }) => { mah='100dvh' style={{ top: viewport.top }} //, transition: 'top 0.1s ease-in-out' }} > -
+
= ({ children }) => { style={{ transition: 'none' }} > - {children} + + {children} + diff --git a/src/features/core/components/pullable.tsx b/src/features/core/components/pullable.tsx index d3e3599..b38e951 100644 --- a/src/features/core/components/pullable.tsx +++ b/src/features/core/components/pullable.tsx @@ -1,9 +1,9 @@ import { ActionIcon, Box, Button, Flex, ScrollArea } from "@mantine/core"; import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react"; import useAppShellHeight from "@/hooks/use-appshell-height"; -import useRefreshConfig from "@/features/core/hooks/use-refresh-config"; import { ArrowClockwiseIcon, SpinnerIcon } from "@phosphor-icons/react"; import { useQueryClient } from "@tanstack/react-query"; +import useRouterConfig from "../hooks/use-router-config"; const THRESHOLD = 80; @@ -20,20 +20,20 @@ const Pullable: React.FC = ({ children, scrollPosition, onScrollP const height = useAppShellHeight(); const [isRefreshing, setIsRefreshing] = useState(false); const [scrolling, setScrolling] = useState(false); - const { toRefresh } = useRefreshConfig(); + const { refresh } = useRouterConfig(); const queryClient = useQueryClient(); const scrollY = useMemo(() => scrollPosition.y < 0 && scrolling ? Math.abs(scrollPosition.y) : 0, [scrollPosition.y, scrolling]); const onTrigger = useCallback(async () => { setIsRefreshing(true); - if (toRefresh.length > 0) { + if (refresh.length > 0) { // TODO: Remove this after testing - or does the delay help ux? await new Promise(resolve => setTimeout(resolve, 1000)); - await queryClient.refetchQueries({ queryKey: toRefresh, exact: true}); + await queryClient.refetchQueries({ queryKey: refresh, exact: true}); } setIsRefreshing(false); - }, [toRefresh]); + }, [refresh]); useEffect(() => { if (!isRefreshing && scrollY > THRESHOLD) { @@ -43,7 +43,7 @@ const Pullable: React.FC = ({ children, scrollPosition, onScrollP const iconOpacity = useMemo(() => { 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 min = 5; @@ -111,7 +111,7 @@ const Pullable: React.FC = ({ children, scrollPosition, onScrollP > { /* TODO: Remove this debug button */} - + {children} diff --git a/src/features/core/hooks/use-header-config.ts b/src/features/core/hooks/use-header-config.ts deleted file mode 100644 index 06c7856..0000000 --- a/src/features/core/hooks/use-header-config.ts +++ /dev/null @@ -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; diff --git a/src/features/core/hooks/use-refresh-config.ts b/src/features/core/hooks/use-refresh-config.ts deleted file mode 100644 index eaed1e4..0000000 --- a/src/features/core/hooks/use-refresh-config.ts +++ /dev/null @@ -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; diff --git a/src/features/core/hooks/use-router-config.ts b/src/features/core/hooks/use-router-config.ts new file mode 100644 index 0000000..e05cc79 --- /dev/null +++ b/src/features/core/hooks/use-router-config.ts @@ -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; diff --git a/src/hooks/use-appshell-height.ts b/src/hooks/use-appshell-height.ts index ed80146..6d809da 100644 --- a/src/hooks/use-appshell-height.ts +++ b/src/hooks/use-appshell-height.ts @@ -1,18 +1,19 @@ import { useMemo } from "react"; 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 isMobile = useIsMobile(); - const headerConfig = useHeaderConfig(); + const { header } = useRouterConfig(); const height = useMemo(() => { const appShellBottomPadding = isMobile ? '70px' : '0px'; const pageBottomPadding = '20px'; - const mobileNavbar = isMobile && !headerConfig.collapsed ? '4rem' : '0px'; + const mobileNavbar = isMobile && !header.collapsed ? '4rem' : '0px'; const pullablePadding = '1.285rem'; return `calc(100dvh - var(--app-shell-header-height, 0px) - ${mobileNavbar} - ${pullablePadding} - ${appShellBottomPadding} - ${pageBottomPadding})`; - }, [isMobile, headerConfig.collapsed]); + }, [isMobile, header.collapsed]); return height; }