add dark mode toggle, uberspace deploy script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -21,6 +21,7 @@
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
@@ -1425,6 +1426,8 @@
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@rollup/plugin-babel/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="],
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
|
||||
19
scripts/deploy.sh
Executable file
19
scripts/deploy.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Description: Build and deploy TherapyFinder to Uberspace static hosting
|
||||
# Usage: ./scripts/deploy.sh <ssh-host>
|
||||
# Example: ./scripts/deploy.sh felixfoertsch@andromeda.uberspace.de
|
||||
|
||||
HOST="${1:?Usage: ./scripts/deploy.sh <ssh-host>}"
|
||||
USER="$(ssh "$HOST" whoami)"
|
||||
REMOTE_DIR="/var/www/virtual/${USER}/html/tpf"
|
||||
|
||||
echo "Building..."
|
||||
bun run build
|
||||
|
||||
echo "Deploying to ${HOST}:${REMOTE_DIR}..."
|
||||
ssh "$HOST" "mkdir -p ${REMOTE_DIR}"
|
||||
rsync -avz --delete dist/ "${HOST}:${REMOTE_DIR}/"
|
||||
|
||||
echo "Done."
|
||||
@@ -1,33 +1,59 @@
|
||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
|
||||
import { useThemeStore } from "../shared/hooks/use-theme";
|
||||
|
||||
const themeIcon = {
|
||||
light: "\u2600",
|
||||
dark: "\u263D",
|
||||
system: "\u2699",
|
||||
} as const;
|
||||
const themeNext = {
|
||||
light: "dark",
|
||||
dark: "system",
|
||||
system: "light",
|
||||
} as const;
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<div className="flex min-h-screen flex-col bg-background text-foreground">
|
||||
<main className="mx-auto w-full max-w-2xl flex-1 px-4 py-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
<nav className="border-t bg-background">
|
||||
<div className="mx-auto flex max-w-2xl">
|
||||
<Link
|
||||
to="/prozess"
|
||||
className="flex-1 py-3 text-center text-sm [&.active]:font-bold [&.active]:text-primary"
|
||||
component: () => {
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background text-foreground">
|
||||
<header className="flex items-center justify-end px-4 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme(themeNext[theme])}
|
||||
className="rounded-md p-1.5 text-lg text-muted-foreground hover:text-foreground"
|
||||
aria-label={`Theme: ${theme}`}
|
||||
>
|
||||
Fortschritt
|
||||
</Link>
|
||||
<Link
|
||||
to="/kontakte"
|
||||
className="flex-1 py-3 text-center text-sm [&.active]:font-bold [&.active]:text-primary"
|
||||
>
|
||||
Kontakte
|
||||
</Link>
|
||||
<Link
|
||||
to="/antrag"
|
||||
className="flex-1 py-3 text-center text-sm [&.active]:font-bold [&.active]:text-primary"
|
||||
>
|
||||
Antrag
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
),
|
||||
{themeIcon[theme]}
|
||||
</button>
|
||||
</header>
|
||||
<main className="mx-auto w-full max-w-2xl flex-1 px-4 py-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
<nav className="border-t bg-background">
|
||||
<div className="mx-auto flex max-w-2xl">
|
||||
<Link
|
||||
to="/prozess"
|
||||
className="flex-1 py-3 text-center text-sm [&.active]:font-bold [&.active]:text-primary"
|
||||
>
|
||||
Fortschritt
|
||||
</Link>
|
||||
<Link
|
||||
to="/kontakte"
|
||||
className="flex-1 py-3 text-center text-sm [&.active]:font-bold [&.active]:text-primary"
|
||||
>
|
||||
Kontakte
|
||||
</Link>
|
||||
<Link
|
||||
to="/antrag"
|
||||
className="flex-1 py-3 text-center text-sm [&.active]:font-bold [&.active]:text-primary"
|
||||
>
|
||||
Antrag
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
32
src/shared/hooks/use-theme.ts
Normal file
32
src/shared/hooks/use-theme.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
|
||||
interface ThemeStore {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeStore>((set) => ({
|
||||
theme: (localStorage.getItem("theme") as Theme) ?? "system",
|
||||
setTheme: (theme) => {
|
||||
localStorage.setItem("theme", theme);
|
||||
set({ theme });
|
||||
applyTheme(theme);
|
||||
},
|
||||
}));
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
const root = document.documentElement;
|
||||
if (theme === "system") {
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
root.classList.toggle("dark", prefersDark);
|
||||
} else {
|
||||
root.classList.toggle("dark", theme === "dark");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply on load
|
||||
applyTheme(useThemeStore.getState().theme);
|
||||
Reference in New Issue
Block a user