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 (
+
+ )
+}
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 (
<>
-
+
+
+
+
+
+