diff --git a/frontend/src/components/layout/footer.tsx b/frontend/src/components/layout/footer.tsx new file mode 100644 index 0000000..498ff77 --- /dev/null +++ b/frontend/src/components/layout/footer.tsx @@ -0,0 +1,27 @@ +import type { Globals, Menu } from '@/lib/types' + +type Props = { + globals: Globals + menu: Menu | undefined +} + +export function Footer({ globals, menu }: Props) { + return ( + + ) +} diff --git a/frontend/src/components/layout/header.tsx b/frontend/src/components/layout/header.tsx new file mode 100644 index 0000000..ac13a25 --- /dev/null +++ b/frontend/src/components/layout/header.tsx @@ -0,0 +1,34 @@ +import { Link } from '@tanstack/react-router' +import type { Globals, Menu } from '@/lib/types' +import { ThemeToggle } from './theme-toggle' + +type Props = { + globals: Globals + menu: Menu | undefined +} + +export function Header({ globals, menu }: Props) { + return ( +
+
+ + {globals.site_name} + + +
+ +
+
+
+ ) +} diff --git a/frontend/src/components/layout/shell.tsx b/frontend/src/components/layout/shell.tsx new file mode 100644 index 0000000..4b2b121 --- /dev/null +++ b/frontend/src/components/layout/shell.tsx @@ -0,0 +1,20 @@ +import { useSuspenseQuery } from '@tanstack/react-query' +import { globalsQuery, menusQuery } from '@/lib/queries' +import { Header } from './header' +import { Footer } from './footer' + +export function Shell({ children }: { children: React.ReactNode }) { + const { data: globals } = useSuspenseQuery(globalsQuery) + const { data: menus } = useSuspenseQuery(menusQuery) + + const mainMenu = menus.find((m) => m.id === 'MAIN_MENU') + const footerMenu = menus.find((m) => m.id === 'FOOTER_MENU') + + return ( +
+
+
{children}
+
+ ) +} diff --git a/frontend/src/components/layout/theme-toggle.tsx b/frontend/src/components/layout/theme-toggle.tsx new file mode 100644 index 0000000..7b80fc5 --- /dev/null +++ b/frontend/src/components/layout/theme-toggle.tsx @@ -0,0 +1,12 @@ +import { Moon, Sun } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { useTheme } from '@/components/theme-provider' + +export function ThemeToggle() { + const { theme, toggle } = useTheme() + return ( + + ) +} diff --git a/frontend/src/components/theme-provider.tsx b/frontend/src/components/theme-provider.tsx new file mode 100644 index 0000000..6cc0644 --- /dev/null +++ b/frontend/src/components/theme-provider.tsx @@ -0,0 +1,49 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'light' | 'dark' + +type ThemeContextValue = { + theme: Theme + toggle: () => void + set: (t: Theme) => void +} + +const ThemeContext = createContext(null) + +const STORAGE_KEY = 'theme' + +function readInitial(): Theme { + if (typeof window === 'undefined') return 'light' + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark') return stored + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState(readInitial) + + useEffect(() => { + const root = document.documentElement + root.classList.remove('light', 'dark') + root.classList.add(theme) + localStorage.setItem(STORAGE_KEY, theme) + }, [theme]) + + return ( + setTheme((t) => (t === 'light' ? 'dark' : 'light')), + set: setTheme, + }} + > + {children} + + ) +} + +export function useTheme() { + const ctx = useContext(ThemeContext) + if (!ctx) throw new Error('useTheme must be used inside ThemeProvider') + return ctx +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9d8d84b..4e84cc8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { RouterProvider, createRouter } from '@tanstack/react-router' import { HelmetProvider } from 'react-helmet-async' import { routeTree } from '@generated/tanstack-router/routeTree.gen' +import { ThemeProvider } from './components/theme-provider' import './index.css' const queryClient = new QueryClient({ @@ -31,9 +32,11 @@ declare module '@tanstack/react-router' { createRoot(document.getElementById('root')!).render( - - - + + + + + , ) diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 8268e83..8ae33fe 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,21 +1,22 @@ import { Outlet, createRootRouteWithContext } from '@tanstack/react-router' import type { QueryClient } from '@tanstack/react-query' import { lazy, Suspense } from 'react' +import { Helmet } from 'react-helmet-async' +import { useSuspenseQuery } from '@tanstack/react-query' import { globalsQuery, menusQuery } from '@/lib/queries' +import { Shell } from '@/components/layout/shell' -const TanStackRouterDevtools = - import.meta.env.PROD - ? () => null - : lazy(() => - import('@tanstack/react-router-devtools').then((m) => ({ default: m.TanStackRouterDevtools })), - ) +const TanStackRouterDevtools = import.meta.env.PROD + ? () => null + : lazy(() => + import('@tanstack/react-router-devtools').then((m) => ({ default: m.TanStackRouterDevtools })), + ) -const ReactQueryDevtools = - import.meta.env.PROD - ? () => null - : lazy(() => - import('@tanstack/react-query-devtools').then((m) => ({ default: m.ReactQueryDevtools })), - ) +const ReactQueryDevtools = import.meta.env.PROD + ? () => null + : lazy(() => + import('@tanstack/react-query-devtools').then((m) => ({ default: m.ReactQueryDevtools })), + ) export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ loader: async ({ context: { queryClient } }) => { @@ -25,9 +26,15 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( }) function RootComponent() { + const { data: globals } = useSuspenseQuery(globalsQuery) return ( <> - + + + + + +