Skip to content

Commit d453064

Browse files
committed
feat: Add client
1 parent d7a21a7 commit d453064

46 files changed

Lines changed: 6220 additions & 36 deletions

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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,37 @@ on:
33
push:
44
branches: [ "**" ]
55
jobs:
6+
build-client:
7+
runs-on: ubuntu-latest
8+
defaults:
9+
run:
10+
working-directory: customer-client
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v5
14+
15+
- name: Set up Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: '22'
19+
cache: npm
20+
cache-dependency-path: customer-client/package-lock.json
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Check Prettier formatting
26+
run: npm run format:check
27+
28+
- name: Lint
29+
run: npm run lint
30+
31+
- name: Build
32+
run: npm run build
33+
634
build:
735
runs-on: ubuntu-latest
36+
needs: build-client
837
steps:
938
- name: Checkout
1039
uses: actions/checkout@v5
@@ -16,6 +45,21 @@ jobs:
1645
distribution: 'temurin'
1746
cache: maven
1847

48+
- name: Set up Node.js
49+
uses: actions/setup-node@v4
50+
with:
51+
node-version: '22'
52+
cache: npm
53+
cache-dependency-path: customer-client/package-lock.json
54+
55+
- name: Install customer-client dependencies
56+
run: npm ci
57+
working-directory: customer-client
58+
59+
- name: Build customer-client
60+
run: npm run build
61+
working-directory: customer-client
62+
1963
- name: Build customer-service
2064
run: mvn clean package -f customer-service/pom.xml -Dservice.name=customer-service
2165

@@ -34,5 +78,28 @@ jobs:
3478
- name: Start with Docker
3579
run: docker compose up -d
3680

81+
- name: Wait for customer-service
82+
run: |
83+
until curl -sf http://localhost:8181/customers/ > /dev/null; do
84+
echo "Waiting for customer-service..."
85+
sleep 2
86+
done
87+
88+
- name: Install Playwright browsers
89+
run: npx playwright install --with-deps chromium
90+
working-directory: customer-client
91+
92+
- name: Run Playwright tests
93+
run: npm run test
94+
working-directory: customer-client
95+
96+
- name: Upload Playwright report
97+
uses: actions/upload-artifact@v4
98+
if: failure()
99+
with:
100+
name: playwright-report
101+
path: customer-client/playwright-report/
102+
37103
- name: Stop Docker Containers
104+
if: always()
38105
run: docker compose down

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
}

address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public boolean isValid(Address address) {
6060

6161
public List<City> findSuggestions(City city) {
6262
ZipCode zipCode = city.getZipCode();
63-
return cities.stream().filter(c -> c.getZipCode().equals(zipCode)).collect(toList());
63+
List<City> suggestions = cities.stream().filter(c -> c.getZipCode().equals(zipCode)).collect(toList());
64+
if (suggestions.isEmpty()) {
65+
CityName name = city.getCityName();
66+
suggestions = cities.stream().filter(c -> c.getCityNames().contains(name)).collect(toList());
67+
}
68+
return suggestions;
6469
}
6570
}

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
# Playwright
16+
playwright-report
17+
test-results
18+
19+
# Editor directories and files
20+
.vscode/*
21+
!.vscode/extensions.json
22+
.idea
23+
.DS_Store
24+
*.suo
25+
*.ntvs*
26+
*.njsproj
27+
*.sln
28+
*.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

0 commit comments

Comments
 (0)