Skip to content

Commit a425147

Browse files
author
Christoph Henrici
committed
CUD Funktionallitaet Frontend + Mock
1 parent cd33758 commit a425147

File tree

20 files changed

+614
-281
lines changed

20 files changed

+614
-281
lines changed

frontend/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
VITE_USE_MOCK=false
1+
VITE_USE_MOCK=true
22
# If you want real backend in dev, flip to false and (optionally) set:
33
VITE_BACKEND_URL=http://localhost:8080

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"react": "19.1.1",
2525
"react-dom": "19.1.1",
2626
"react-error-boundary": "6.0.0",
27-
"react-router": "7.8.2",
27+
"react-router-dom": "7.8.2",
2828
"valibot": "1.1.0"
2929
},
3030
"devDependencies": {

frontend/pnpm-lock.yaml

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {lazy, Suspense} from 'react'
22
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
3-
import {Route, Routes} from 'react-router'
3+
import {Route, Routes} from 'react-router-dom'
44
import {LoadingOrError} from '@/components/LoadingOrError'
55
import {Gallery} from '@/pages/Gallery'
66
import { NewClient } from './pages/NewClient'

frontend/src/api/clients.ts

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,63 @@
1-
import * as v from 'valibot'
1+
import * as v from 'valibot';
22

3-
const Client = v.object({
4-
id: v.number(),
5-
name: v.string(),
6-
email: v.string()
7-
})
8-
export type Client = v.InferOutput<typeof Client>
3+
const Client = v.object({ id: v.number(), name: v.string(), email: v.string() });
4+
export type Client = v.InferOutput<typeof Client>;
5+
const Clients = v.array(Client);
96

10-
const Clients = v.array(Client)
7+
// Write DTO (what your form sends)
8+
const ClientUpsert = v.object({
9+
name: v.string(),
10+
email: v.string()
11+
});
12+
export type ClientUpsert = v.InferOutput<typeof ClientUpsert>;
13+
14+
const API = '/api';
1115

1216
export async function getClients() {
13-
const response = await fetch('/api/clients', {credentials: 'include'})
14-
if (!response.ok) {
15-
throw new Error('Failed to fetch Clients')
16-
}
17-
return v.parse(Clients, await response.json())
17+
const r = await fetch(`${API}/clients`, { credentials: 'include' });
18+
if (!r.ok) throw new Error('Failed to fetch clients');
19+
return v.parse(Clients, await r.json());
20+
}
21+
22+
export async function getClient(id: number) {
23+
const r = await fetch(`${API}/clients/${id}`, { credentials: 'include' });
24+
if (!r.ok) throw new Error(`Client ${id} not found`);
25+
return v.parse(Client, await r.json());
26+
}
27+
28+
export async function createClient(input: ClientUpsert) {
29+
// optional client-side shape check
30+
v.parse(ClientUpsert, input);
31+
32+
const r = await fetch(`${API}/clients`, {
33+
method: 'POST',
34+
credentials: 'include',
35+
headers: { 'Content-Type': 'application/json' },
36+
body: JSON.stringify(input)
37+
});
38+
if (r.status === 409) throw new Error('Email already exists');
39+
if (!r.ok) throw new Error('Create failed');
40+
return v.parse(Client, await r.json());
1841
}
42+
43+
export async function updateClient(id: number, input: ClientUpsert) {
44+
v.parse(ClientUpsert, input);
45+
46+
const r = await fetch(`${API}/clients/${id}`, {
47+
method: 'PUT',
48+
credentials: 'include',
49+
headers: { 'Content-Type': 'application/json' },
50+
body: JSON.stringify(input)
51+
});
52+
if (r.status === 409) throw new Error('Email already exists');
53+
if (!r.ok) throw new Error('Update failed');
54+
return v.parse(Client, await r.json());
55+
}
56+
57+
export async function deleteClient(id: number) {
58+
const r = await fetch(`${API}/clients/${id}`, {
59+
method: 'DELETE',
60+
credentials: 'include'
61+
});
62+
if (!(r.ok || r.status === 204)) throw new Error('Delete failed');
63+
}

frontend/src/components/Clients.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Link} from 'react-router'
1+
import {Link} from 'react-router-dom'
22
import type {Client as ClientType} from '@/api/clients'
33

44
interface Properties {

frontend/src/components/ClientsTable.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
1-
// src/components/ClientTable.tsx
2-
import { Link } from 'react-router';
3-
import type {Client as ClientType} from '@/api/clients'
1+
// src/components/ClientsTable.tsx
2+
import { Link } from 'react-router-dom';
3+
import { useMutation, useQueryClient } from '@tanstack/react-query';
4+
import { deleteClient, type Client as ClientType } from '@/api/clients';
45

56
export function ClientsTable({ clients }: { clients: ClientType[] }) {
7+
const qc = useQueryClient();
8+
9+
const del = useMutation({
10+
mutationFn: (id: number) => deleteClient(id),
11+
onMutate: async (id) => {
12+
await qc.cancelQueries({ queryKey: ['clients'] });
13+
const prevClients = qc.getQueryData<ClientType[]>(['clients']);
14+
// optimistic remove
15+
qc.setQueryData<ClientType[]>(['clients'], (old = []) => old.filter(c => c.id !== id));
16+
// optimistic tenantInfo-- (optional, see next block)
17+
const prevTenant = qc.getQueryData<any>(['tenantInfo']);
18+
if (prevTenant) {
19+
qc.setQueryData(['tenantInfo'], { ...prevTenant, clientCount: Math.max(0, (prevTenant.clientCount ?? 0) - 1) });
20+
}
21+
return { prevClients, prevTenant };
22+
},
23+
onError: (_err, _id, ctx) => {
24+
if (ctx?.prevClients) qc.setQueryData(['clients'], ctx.prevClients);
25+
if (ctx?.prevTenant) qc.setQueryData(['tenantInfo'], ctx.prevTenant);
26+
},
27+
onSettled: () => {
28+
qc.invalidateQueries({ queryKey: ['clients'] });
29+
qc.invalidateQueries({ queryKey: ['tenantInfo'] }); // <-- important
30+
},
31+
});
32+
633
return (
734
<div className="overflow-x-auto rounded-lg border border-gray-300 bg-white shadow-sm">
835
<table className="min-w-full border-collapse text-sm">
@@ -29,15 +56,23 @@ export function ClientsTable({ clients }: { clients: ClientType[] }) {
2956
Edit
3057
</Link>
3158
<button
32-
disabled
33-
className="cursor-not-allowed text-gray-400"
34-
title="Delete (disabled for now)"
59+
className="text-red-600 hover:text-red-700 font-medium disabled:opacity-60"
60+
onClick={() => del.mutate(c.id)}
61+
disabled={del.isPending}
62+
title="Delete"
3563
>
36-
Delete
64+
{del.isPending ? 'Deleting…' : 'Delete'}
3765
</button>
3866
</td>
3967
</tr>
4068
))}
69+
{clients.length === 0 && (
70+
<tr>
71+
<td colSpan={3} className="px-6 py-6 text-gray-600">
72+
No clients yet.
73+
</td>
74+
</tr>
75+
)}
4176
</tbody>
4277
</table>
4378
</div>
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { getTenantInfo} from '@/api/tenantInfo'
2-
import { useSuspenseQuery } from '@tanstack/react-query'
2+
import { useQuery } from '@tanstack/react-query'
33

44
export function TenantBadge() {
5-
const { data } = useSuspenseQuery({
5+
const { data } = useQuery({
66
queryFn: getTenantInfo,
77
queryKey: ['tenantInfo']
88
})
99
if (!data?.tenantDisplay) return null
1010
return (
1111
<span className="text-xs text-gray-500">
1212
Workspace: <span className="font-mono">{data.tenantDisplay}</span>
13-
{typeof data.clientCount === 'number' && <> · {data.clientCount} clients</>}
13+
{typeof data.clientCount === 'number' && <> · {data?.clientCount ?? 0} clients</>}
1414
</span>
1515
)
16-
}
16+
}

frontend/src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './global.css';
22
import { StrictMode } from 'react';
33
import { createRoot } from 'react-dom/client';
4-
import { HashRouter } from 'react-router';
4+
import { HashRouter } from 'react-router-dom';
55
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
66
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
77
import { App } from './App';

frontend/src/mocks/data/clients.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"id": 2,
9-
"name": "Another one ",
9+
"name": "Another one",
1010
"email": "another©example.com"
1111
},
1212
{

0 commit comments

Comments
 (0)