diff --git a/apps/admin/.gitignore b/apps/admin/.gitignore
new file mode 100644
index 0000000..542cb8e
--- /dev/null
+++ b/apps/admin/.gitignore
@@ -0,0 +1,2 @@
+dist
+*.tsbuildinfo
diff --git a/apps/admin/index.html b/apps/admin/index.html
new file mode 100644
index 0000000..42c3b44
--- /dev/null
+++ b/apps/admin/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ SoleLog Admin
+
+
+
+
+
+
diff --git a/apps/admin/package.json b/apps/admin/package.json
new file mode 100644
index 0000000..b6ed4db
--- /dev/null
+++ b/apps/admin/package.json
@@ -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"
+ }
+}
diff --git a/apps/admin/src/App.test.tsx b/apps/admin/src/App.test.tsx
new file mode 100644
index 0000000..187343a
--- /dev/null
+++ b/apps/admin/src/App.test.tsx
@@ -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(
+
+
+
+ );
+}
+
+describe('App', () => {
+ it('renders the admin app name', () => {
+ renderApp();
+ expect(screen.getByText('SoleLog Admin')).toBeInTheDocument();
+ });
+});
diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx
new file mode 100644
index 0000000..5fa5b84
--- /dev/null
+++ b/apps/admin/src/App.tsx
@@ -0,0 +1,3 @@
+export default function App() {
+ return SoleLog Admin
;
+}
diff --git a/apps/admin/src/lib/api.ts b/apps/admin/src/lib/api.ts
new file mode 100644
index 0000000..27b6469
--- /dev/null
+++ b/apps/admin/src/lib/api.ts
@@ -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(path: string, init: RequestInit = {}): Promise {
+ 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 {
+ 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);
+}
diff --git a/apps/admin/src/lib/auth-storage.ts b/apps/admin/src/lib/auth-storage.ts
new file mode 100644
index 0000000..71c7cc1
--- /dev/null
+++ b/apps/admin/src/lib/auth-storage.ts
@@ -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);
+}
diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx
new file mode 100644
index 0000000..2fb2974
--- /dev/null
+++ b/apps/admin/src/main.tsx
@@ -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(
+
+
+
+
+
+);
diff --git a/apps/admin/src/styles.css b/apps/admin/src/styles.css
new file mode 100644
index 0000000..e69de29
diff --git a/apps/admin/src/test/setup.ts b/apps/admin/src/test/setup.ts
new file mode 100644
index 0000000..7b0828b
--- /dev/null
+++ b/apps/admin/src/test/setup.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom';
diff --git a/apps/admin/src/vite-env.d.ts b/apps/admin/src/vite-env.d.ts
new file mode 100644
index 0000000..c57d674
--- /dev/null
+++ b/apps/admin/src/vite-env.d.ts
@@ -0,0 +1,9 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/apps/admin/tsconfig.app.json b/apps/admin/tsconfig.app.json
new file mode 100644
index 0000000..8e71c4c
--- /dev/null
+++ b/apps/admin/tsconfig.app.json
@@ -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"]
+}
diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json
new file mode 100644
index 0000000..d32ff68
--- /dev/null
+++ b/apps/admin/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
+}
diff --git a/apps/admin/tsconfig.node.json b/apps/admin/tsconfig.node.json
new file mode 100644
index 0000000..056de49
--- /dev/null
+++ b/apps/admin/tsconfig.node.json
@@ -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"]
+}
diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts
new file mode 100644
index 0000000..43ff302
--- /dev/null
+++ b/apps/admin/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: { host: true, port: 5174 },
+});
diff --git a/apps/admin/vitest.config.ts b/apps/admin/vitest.config.ts
new file mode 100644
index 0000000..8594b6a
--- /dev/null
+++ b/apps/admin/vitest.config.ts
@@ -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'] },
+});
diff --git a/yarn.lock b/yarn.lock
index beac8ff..c501c3d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"