Implement shadcn/ui + Tailwind CSS UI layer

- Design token system via CSS custom properties (light/dark mode)
- Theme context hook + ThemeToggle component
- AppShell + collapsible Sidebar replace inline Nav
- LoginPage redesigned: two-column grid with hero panel
- shadcn/ui Button and Input components
- Tailwind config wired to CSS variable tokens
- All pages de-Nav'd; PrivateRoute/AdminRoute wrap with AppShell
- TypeScript passes clean (npm run typecheck)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-17 12:32:06 +02:00
parent 9e2e4ec338
commit c3f87706ee
26 changed files with 1263 additions and 89 deletions
+1 -8
View File
@@ -1,6 +1,5 @@
import { useEffect, useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import Nav from "../components/Nav";
import { getAISettings, updateAISettings, testAIConnection } from "../api/client";
type Provider = "anthropic" | "ollama" | "lmstudio";
@@ -96,17 +95,11 @@ export default function AIAdminSettingsPage() {
};
if (isLoading) {
return (
<>
<Nav />
<div style={{ padding: 32 }}>Loading</div>
</>
);
return <div style={{ padding: 32 }}>Loading</div>;
}
return (
<>
<Nav />
<div style={{ padding: 32, maxWidth: 600, margin: "0 auto" }}>
<h1 style={{ fontSize: 24, marginBottom: 32 }}>AI Service Settings</h1>
-2
View File
@@ -9,7 +9,6 @@ import {
type AdminUserCreate,
type UserData,
} from "../api/client";
import Nav from "../components/Nav";
export default function AdminPage() {
const queryClient = useQueryClient();
@@ -68,7 +67,6 @@ export default function AdminPage() {
return (
<>
<Nav />
<div style={{ padding: 32, maxWidth: 800 }}>
<h1>User Management</h1>
+1 -2
View File
@@ -1,6 +1,5 @@
import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import Nav from "../components/Nav";
import { getMe } from "../api/client";
interface AppCard {
@@ -36,7 +35,6 @@ export default function AppsPage() {
return (
<>
<Nav />
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
<h1>Apps</h1>
<div style={{ display: "flex", gap: 24, flexWrap: "wrap", marginTop: 24 }}>
@@ -102,3 +100,4 @@ export default function AppsPage() {
</>
);
}
+4 -8
View File
@@ -1,17 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { getMe } from "../api/client";
import Nav from "../components/Nav";
export default function DashboardPage() {
const { data: user } = useQuery({ queryKey: ["me"], queryFn: getMe });
return (
<>
<Nav />
<div style={{ padding: 32 }}>
<h1>Dashboard</h1>
{user && <p>Welcome, {user.full_name ?? user.email}</p>}
</div>
</>
<div>
<h1 className="text-2xl font-semibold text-foreground mb-2">Dashboard</h1>
{user && <p className="text-muted">Welcome, {user.full_name ?? user.email}</p>}
</div>
);
}
@@ -1,6 +1,5 @@
import { useEffect, useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import Nav from "../components/Nav";
import { getDocumentLimits, updateDocumentLimits } from "../api/client";
const inputStyle: React.CSSProperties = {
@@ -34,17 +33,11 @@ export default function DocumentAdminSettingsPage() {
});
if (isLoading) {
return (
<>
<Nav />
<div style={{ padding: 32 }}>Loading</div>
</>
);
return <div style={{ padding: 32 }}>Loading</div>;
}
return (
<>
<Nav />
<div style={{ padding: 32, maxWidth: 600, margin: "0 auto" }}>
<h1 style={{ fontSize: 24, marginBottom: 32 }}>Documents Settings</h1>
-2
View File
@@ -1,6 +1,5 @@
import { useRef, useState, useEffect, useCallback } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Nav from "../components/Nav";
import {
listDocuments,
uploadDocument,
@@ -707,7 +706,6 @@ export default function DocumentsPage() {
return (
<>
<Nav />
<div style={{ padding: 32, maxWidth: 960, margin: "0 auto" }}>
<h1>Documents</h1>
+44 -45
View File
@@ -1,11 +1,13 @@
import axios from "axios";
import { useState } from "react";
import { Briefcase, LayoutDashboard } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import ThemeToggle from "@/components/ThemeToggle";
import { useAuth } from "../hooks/useAuth";
// ── Customise these two constants for each deployment ────────────────────────
// ── Customise for each deployment ────────────────────────────────────────────
const BUSINESS_NAME = "Your Business Name";
// Replace the src with the actual logo URL or import, or swap the placeholder
// div below for an <img> tag once a logo is available.
// ─────────────────────────────────────────────────────────────────────────────
export default function LoginPage() {
@@ -30,62 +32,59 @@ export default function LoginPage() {
};
return (
<div style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<div style={{ width: 360, padding: 32 }}>
{/* Logo placeholder — swap for <img src="..." alt="Logo" /> */}
<div style={{
width: 96,
height: 96,
margin: "0 auto 24px",
border: "2px dashed #aaa",
borderRadius: 8,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#aaa",
fontSize: 12,
}}>
Logo
<div className="grid md:grid-cols-2 min-h-screen">
{/* ── Left column: login form ── */}
<div className="relative flex flex-col items-center justify-center p-8 bg-surface">
{/* Theme toggle — top-right of this panel */}
<div className="absolute top-4 right-4">
<ThemeToggle />
</div>
<h1 style={{ textAlign: "center", marginBottom: 24 }}>{BUSINESS_NAME}</h1>
<div className="w-full max-w-sm space-y-6">
{/* Logo placeholder */}
<div className="flex flex-col items-center gap-3">
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary/10">
<Briefcase className="h-8 w-8 text-primary" />
</div>
<h1 className="text-xl font-semibold text-primary">{BUSINESS_NAME}</h1>
<p className="text-sm text-muted">Sign in to your account</p>
</div>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: 12 }}>
<label style={{ display: "block", marginBottom: 4 }}>Email</label>
<input
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<Input
type="email"
placeholder="you@company.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
style={{ width: "100%", padding: "6px 8px", boxSizing: "border-box" }}
/>
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ display: "block", marginBottom: 4 }}>Password</label>
<input
<Input
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
style={{ width: "100%", padding: "6px 8px", boxSizing: "border-box" }}
/>
</div>
{error && <p style={{ color: "red", margin: "8px 0" }}>{error}</p>}
<button
type="submit"
style={{ width: "100%", padding: "8px 0", marginTop: 8, cursor: "pointer" }}
>
Sign in
</button>
</form>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<Button type="submit" className="w-full">
Sign In
</Button>
</form>
</div>
</div>
{/* ── Right column: hero panel ── */}
<div className="hidden md:flex flex-col items-center justify-center bg-primary p-8 gap-4">
<LayoutDashboard className="h-20 w-20 text-white/80" />
<h2 className="text-2xl font-bold text-white text-center">
Manage your team, your way
</h2>
<p className="text-sm text-white/70 text-center">
The all-in-one employer platform.
</p>
</div>
</div>
);
+1 -3
View File
@@ -1,7 +1,6 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getMe, getProfile, updateProfile, type ProfileUpdate } from "../api/client";
import Nav from "../components/Nav";
export default function ProfilePage() {
const queryClient = useQueryClient();
@@ -54,11 +53,10 @@ export default function ProfilePage() {
mutation.mutate(payload);
};
if (isLoading) return <><Nav /><div style={{ padding: 32 }}>Loading</div></>;
if (isLoading) return <div style={{ padding: 32 }}>Loading</div>;
return (
<>
<Nav />
<div style={{ padding: 32, maxWidth: 480 }}>
<h1>Profile</h1>