Skip to content

Commit 817e879

Browse files
committed
feat: Add client
1 parent c34b81e commit 817e879

41 files changed

Lines changed: 5473 additions & 35 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ jobs:
1616
distribution: 'temurin'
1717
cache: maven
1818

19+
- name: Set up Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '22'
23+
24+
- name: Install customer-client dependencies
25+
run: npm ci
26+
working-directory: customer-client
27+
28+
- name: Check Prettier formatting
29+
run: npm run format:check
30+
working-directory: customer-client
31+
32+
- name: Lint customer-client
33+
run: npm run lint
34+
working-directory: customer-client
35+
36+
- name: Build customer-client
37+
run: npm run build
38+
working-directory: customer-client
39+
1940
- name: Build customer-service
2041
run: mvn clean package -f customer-service/pom.xml -Dservice.name=customer-service
2142

address-validation-service/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@
3636
<scope>provided</scope>
3737
</dependency>
3838
<dependency>
39-
<groupId>org.apache.geronimo.specs</groupId>
40-
<artifactId>geronimo-validation_2.0_spec</artifactId>
41-
<version>1.0</version>
39+
<groupId>jakarta.validation</groupId>
40+
<artifactId>jakarta.validation-api</artifactId>
41+
<version>3.1.1</version>
4242
</dependency>
4343
<dependency>
4444
<groupId>org.eclipse.microprofile.config</groupId>

address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
1515
*/
1616
package de.openknowledge.sample.address.application;
1717

18+
import static jakarta.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE;
19+
import static java.util.Locale.GERMANY;
20+
import static java.util.Optional.ofNullable;
1821
import static java.util.stream.Collectors.joining;
1922

2023
import java.net.URI;
21-
import java.net.URISyntaxException;
2224
import java.util.List;
25+
import java.util.Locale;
2326
import java.util.logging.Logger;
2427

2528
import jakarta.enterprise.context.ApplicationScoped;
2629
import jakarta.inject.Inject;
2730
import jakarta.ws.rs.Consumes;
2831
import jakarta.ws.rs.GET;
32+
import jakarta.ws.rs.HeaderParam;
2933
import jakarta.ws.rs.POST;
3034
import jakarta.ws.rs.Path;
3135
import jakarta.ws.rs.Produces;
@@ -64,10 +68,9 @@ public Response healthCheck() {
6468
@POST
6569
@Path("/")
6670
@Consumes(MediaType.APPLICATION_JSON)
67-
public Response validateAddress(Address address, @Context UriInfo uri) throws URISyntaxException {
71+
public Response validateAddress(Address address, @Context UriInfo uri, @HeaderParam(ACCEPT_LANGUAGE) Locale locale) {
6872
LOGGER.info("RESTful call 'POST valid address'");
6973
if (addressesRepository.isValid(address)) {
70-
LOGGER.fine("address is valid");
7174
return Response.ok().build();
7275
} else {
7376
URI type = uri.getRequestUri().resolve("/errors/invalid-city");
@@ -76,24 +79,49 @@ public Response validateAddress(Address address, @Context UriInfo uri) throws UR
7679
LOGGER.fine(suggestions.size() + " suggestions found: " + suggestions);
7780
if (suggestions.size() == 1) {
7881
return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE)
79-
.entity(String.format(PROBLEM_JSON, type, "invalid city",
82+
.entity(String.format(PROBLEM_JSON, type,
83+
translate("invalid city", locale),
8084
Response.Status.BAD_REQUEST.getStatusCode(),
81-
"Did you mean " + suggestions.iterator().next() + "?", instance))
85+
translate("Did you mean %s?", locale, suggestions.iterator().next()),
86+
instance))
8287
.build();
8388
} else if (!suggestions.isEmpty()) {
89+
String suggestionList = suggestions.stream().map(Object::toString).collect(joining(", "));
8490
return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE)
85-
.entity(String.format(PROBLEM_JSON, type, "invalid city",
91+
.entity(String.format(PROBLEM_JSON, type,
92+
translate("invalid city", locale),
8693
Response.Status.BAD_REQUEST.getStatusCode(),
87-
"Did you mean one of "
88-
+ suggestions.stream().map(Object::toString).collect(joining(", ")) + "?",
94+
translate("Did you mean one of %s?", locale, suggestionList),
8995
instance))
9096
.build();
9197
} else {
9298
return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE)
93-
.entity(String.format(PROBLEM_JSON, type, "invalid city",
94-
Response.Status.BAD_REQUEST.getStatusCode(), "no matching city found", instance))
99+
.entity(String.format(PROBLEM_JSON, type,
100+
translate("invalid city", locale),
101+
Response.Status.BAD_REQUEST.getStatusCode(),
102+
translate("no matching city found",
103+
locale),
104+
instance))
95105
.build();
96106
}
97107
}
98108
}
109+
110+
private String translate(String template, Locale language, Object... parameters) {
111+
if (ofNullable(language).map(Locale::getLanguage).filter(GERMANY.getLanguage()::equals).isPresent()) {
112+
switch (template) {
113+
case "Did you mean %s?":
114+
return "Meinten Sie %s?".formatted(parameters);
115+
case "invalid city":
116+
return "ungültige Stadt";
117+
case "no matching city found":
118+
return "keine passende Stadt gefunden";
119+
case "Did you mean one of %s?":
120+
return "Meinten Sie eine von %s?".formatted(parameters);
121+
default:
122+
return template.formatted(parameters);
123+
}
124+
}
125+
return template.formatted(parameters);
126+
}
99127
}

billing-service/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@
3636
<scope>provided</scope>
3737
</dependency>
3838
<dependency>
39-
<groupId>org.apache.geronimo.specs</groupId>
40-
<artifactId>geronimo-validation_2.0_spec</artifactId>
41-
<version>1.0</version>
39+
<groupId>jakarta.validation</groupId>
40+
<artifactId>jakarta.validation-api</artifactId>
41+
<version>3.1.1</version>
4242
</dependency>
4343
<dependency>
4444
<groupId>org.eclipse.microprofile.config</groupId>

customer-client/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

customer-client/.prettierrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"semi": true,
3+
"singleQuote": true,
4+
"trailingComma": "all",
5+
"printWidth": 100,
6+
"tabWidth": 2
7+
}

customer-client/Dockerfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Build Stage
2+
FROM node:20-alpine AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy package files
7+
COPY package*.json ./
8+
9+
# Install dependencies
10+
RUN npm ci
11+
12+
# Copy source code
13+
COPY . .
14+
15+
# Build argument for API URL (can be overridden at build time)
16+
ARG VITE_API_URL=http://localhost:8181
17+
ENV VITE_API_URL=$VITE_API_URL
18+
19+
# Build the application
20+
RUN npm run build
21+
22+
# Production Stage
23+
FROM nginx:alpine
24+
25+
# Copy built assets from builder stage
26+
COPY --from=builder /app/dist /usr/share/nginx/html
27+
28+
# Copy nginx configuration for SPA routing
29+
RUN echo 'server { \
30+
listen 80; \
31+
server_name localhost; \
32+
root /usr/share/nginx/html; \
33+
index index.html; \
34+
location / { \
35+
try_files $uri $uri/ /index.html; \
36+
} \
37+
}' > /etc/nginx/conf.d/default.conf
38+
39+
EXPOSE 80
40+
41+
CMD ["nginx", "-g", "daemon off;"]

customer-client/README-APP.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Kundenpflege App
2+
3+
Eine React-basierte Web-Applikation zur Kundenverwaltung mit Master-Detail-Ansicht.
4+
5+
## Tech Stack
6+
7+
- **React 19** - UI Framework
8+
- **TypeScript** - Type Safety
9+
- **Vite** - Build Tool
10+
- **Tanstack Query** - Server State Management
11+
- **Tanstack Table** - Tabellen-Komponente
12+
- **Tanstack Forms** - Formular-Management
13+
- **Zod** - Schema-Validierung
14+
- **React Router** - Client-seitiges Routing
15+
16+
## Features
17+
18+
- **Kundenliste (Master-Ansicht)**: Übersicht aller Kunden mit Sortierung
19+
- **Kundendetails (Detail-Ansicht)**: Detailansicht eines einzelnen Kunden
20+
- **Kunde anlegen**: Formular zum Erstellen neuer Kunden
21+
- **Adressen bearbeiten**: Rechnungs- und Lieferadresse hinzufügen/bearbeiten
22+
- **Validierung**: Client-seitige Validierung mit Zod
23+
24+
## Voraussetzungen
25+
26+
Das Backend (customer-service) muss auf `http://localhost:8080` laufen.
27+
28+
## Installation & Start
29+
30+
```bash
31+
# Dependencies installieren
32+
npm install
33+
34+
# Entwicklungsserver starten
35+
npm run dev
36+
37+
# Build für Produktion
38+
npm run build
39+
40+
# Build-Preview
41+
npm run preview
42+
```
43+
44+
## Umgebungsvariablen
45+
46+
`.env` Datei:
47+
48+
```
49+
VITE_API_URL=http://localhost:8080/api
50+
```
51+
52+
## API-Endpunkte
53+
54+
Die App nutzt folgende Backend-Endpunkte:
55+
56+
- `GET /api/customers/` - Alle Kunden abrufen
57+
- `POST /api/customers/` - Neuen Kunden erstellen
58+
- `GET /api/customers/{customerNumber}` - Einzelnen Kunden abrufen
59+
- `PUT /api/customers/{customerNumber}/billing-address` - Rechnungsadresse aktualisieren
60+
- `PUT /api/customers/{customerNumber}/delivery-address` - Lieferadresse aktualisieren
61+
62+
## Projektstruktur
63+
64+
```
65+
src/
66+
├── api/ # API-Client und React Query Hooks
67+
├── components/ # Wiederverwendbare Komponenten (Formulare)
68+
├── pages/ # Seiten-Komponenten (Routen)
69+
├── types/ # TypeScript-Typen und Zod-Schemas
70+
├── App.tsx # Haupt-App-Komponente mit Routing
71+
├── App.css # Globale Styles
72+
└── main.tsx # Entry Point
73+
```
74+
75+
## Entwickelt mit
76+
77+
Claude Code - AI-gestützte Entwicklung

customer-client/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# React + TypeScript + Vite
2+
3+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+
5+
Currently, two official plugins are available:
6+
7+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9+
10+
## React Compiler
11+
12+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13+
14+
## Expanding the ESLint configuration
15+
16+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17+
18+
```js
19+
export default defineConfig([
20+
globalIgnores(['dist']),
21+
{
22+
files: ['**/*.{ts,tsx}'],
23+
extends: [
24+
// Other configs...
25+
26+
// Remove tseslint.configs.recommended and replace with this
27+
tseslint.configs.recommendedTypeChecked,
28+
// Alternatively, use this for stricter rules
29+
tseslint.configs.strictTypeChecked,
30+
// Optionally, add this for stylistic rules
31+
tseslint.configs.stylisticTypeChecked,
32+
33+
// Other configs...
34+
],
35+
languageOptions: {
36+
parserOptions: {
37+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
38+
tsconfigRootDir: import.meta.dirname,
39+
},
40+
// other options...
41+
},
42+
},
43+
]);
44+
```
45+
46+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47+
48+
```js
49+
// eslint.config.js
50+
import reactX from 'eslint-plugin-react-x';
51+
import reactDom from 'eslint-plugin-react-dom';
52+
53+
export default defineConfig([
54+
globalIgnores(['dist']),
55+
{
56+
files: ['**/*.{ts,tsx}'],
57+
extends: [
58+
// Other configs...
59+
// Enable lint rules for React
60+
reactX.configs['recommended-typescript'],
61+
// Enable lint rules for React DOM
62+
reactDom.configs.recommended,
63+
],
64+
languageOptions: {
65+
parserOptions: {
66+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
67+
tsconfigRootDir: import.meta.dirname,
68+
},
69+
// other options...
70+
},
71+
},
72+
]);
73+
```

customer-client/eslint.config.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import js from '@eslint/js';
2+
import globals from 'globals';
3+
import reactHooks from 'eslint-plugin-react-hooks';
4+
import reactRefresh from 'eslint-plugin-react-refresh';
5+
import tseslint from 'typescript-eslint';
6+
import { defineConfig, globalIgnores } from 'eslint/config';
7+
import eslintConfigPrettier from 'eslint-config-prettier';
8+
9+
export default defineConfig([
10+
globalIgnores(['dist']),
11+
{
12+
files: ['**/*.{ts,tsx}'],
13+
extends: [
14+
js.configs.recommended,
15+
tseslint.configs.recommended,
16+
reactHooks.configs.flat.recommended,
17+
reactRefresh.configs.vite,
18+
eslintConfigPrettier,
19+
],
20+
languageOptions: {
21+
ecmaVersion: 2020,
22+
globals: globals.browser,
23+
},
24+
},
25+
]);

0 commit comments

Comments
 (0)