feat(admin): scaffold Vite+React admin workspace

This commit is contained in:
Bas van Rossem
2026-06-17 18:56:28 +02:00
parent 02b7522b87
commit 682a9dce44
17 changed files with 219 additions and 0 deletions

2
apps/admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dist
*.tsbuildinfo

15
apps/admin/index.html Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="UTF-8" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#2563EB" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<link rel="apple-touch-icon" href="/icon-192.png" />
<title>SoleLog Admin</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

34
apps/admin/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "@solelog/admin",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@solelog/shared": "workspace:*",
"@tanstack/react-query": "^5.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"jsdom": "^25.0.0",
"typescript": "^5.7.2",
"vite": "^7.0.0",
"vitest": "^3.0.0"
}
}

View File

@@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
function renderApp() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}
describe('App', () => {
it('renders the admin app name', () => {
renderApp();
expect(screen.getByText('SoleLog Admin')).toBeInTheDocument();
});
});

3
apps/admin/src/App.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function App() {
return <div>SoleLog Admin</div>;
}

37
apps/admin/src/lib/api.ts Normal file
View File

@@ -0,0 +1,37 @@
import { getToken, setToken } from './auth-storage';
export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
export class ApiError extends Error {
constructor(
public status: number,
message: string
) {
super(message);
this.name = 'ApiError';
}
}
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
const token = getToken();
const headers = new Headers(init.headers);
if (token) headers.set('Authorization', `Bearer ${token}`);
if (init.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
const res = await fetch(`${API_URL}${path}`, { ...init, headers });
if (!res.ok) throw new ApiError(res.status, `Request failed: ${res.status}`);
const text = await res.text();
return (text ? JSON.parse(text) : undefined) as T;
}
// Sign in: POST /api/auth/sign-in/email, capture the bearer token from the response header.
export async function signIn(email: string, password: string): Promise<void> {
const res = await fetch(`${API_URL}/api/auth/sign-in/email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new ApiError(res.status, 'Inloggen mislukt');
const token = res.headers.get('set-auth-token');
if (!token) throw new ApiError(500, 'Geen token ontvangen');
setToken(token);
}

View File

@@ -0,0 +1,13 @@
const TOKEN_KEY = 'solelog.token';
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
export function setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token);
}
export function clearToken(): void {
localStorage.removeItem(TOKEN_KEY);
}

15
apps/admin/src/main.tsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './styles.css';
const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);

View File

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom';

9
apps/admin/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src"]
}

4
apps/admin/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"skipLibCheck": true,
"types": ["node"]
},
"include": ["vite.config.ts", "vitest.config.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { host: true, port: 5174 },
});

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'] },
});

View File

@@ -1761,6 +1761,29 @@ __metadata:
languageName: node
linkType: hard
"@solelog/admin@workspace:apps/admin":
version: 0.0.0-use.local
resolution: "@solelog/admin@workspace:apps/admin"
dependencies:
"@solelog/shared": "workspace:*"
"@tanstack/react-query": "npm:^5.0.0"
"@testing-library/dom": "npm:^10.4.1"
"@testing-library/jest-dom": "npm:^6.4.0"
"@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.5.0"
"@types/react": "npm:^18.3.0"
"@types/react-dom": "npm:^18.3.0"
"@vitejs/plugin-react": "npm:^4.3.0"
jsdom: "npm:^25.0.0"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
react-router-dom: "npm:^6.26.0"
typescript: "npm:^5.7.2"
vite: "npm:^7.0.0"
vitest: "npm:^3.0.0"
languageName: unknown
linkType: soft
"@solelog/api@workspace:apps/api":
version: 0.0.0-use.local
resolution: "@solelog/api@workspace:apps/api"