Compare commits
15 Commits
471c0c7a49
...
v2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a88c141f28 | |||
| 843a5d07d9 | |||
| 4008b3de94 | |||
| dbacc172d8 | |||
| 08cb38df8f | |||
| 8650745009 | |||
| 5c035ba1c0 | |||
| 746530ae4c | |||
| 3fd0889ebf | |||
| e3a26de92d | |||
| 75b75169a8 | |||
| 1b4cc81e61 | |||
| 2a4825269b | |||
| 4df6d243f3 | |||
| ab9cfd6cbf |
+2
-1
@@ -155,8 +155,9 @@ backend/public/uploads/
|
|||||||
secrets/
|
secrets/
|
||||||
keys/
|
keys/
|
||||||
|
|
||||||
|
# Own files
|
||||||
ToDo.txt
|
ToDo.txt
|
||||||
|
PayPal-QR-Code.png
|
||||||
|
|
||||||
# only in development branch
|
# only in development branch
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2026 theis.gaedigk
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,2 +1,164 @@
|
|||||||
# ca-lose
|
# CA-Lose
|
||||||
|
|
||||||
|
CA-Lose is a small full-stack app for collecting ticket-sale/order form entries.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Frontend**: React + TypeScript + Vite (UI uses MUI + Tailwind, i18n via i18next)
|
||||||
|
- **Backend**: Node.js + Express (also renders a minimal EJS page at `/`)
|
||||||
|
- **Database**: MySQL 8
|
||||||
|
|
||||||
|
In production, the frontend is served by **Nginx** and proxies `/backend/*` to the backend container.
|
||||||
|
|
||||||
|
## Repos & folders
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/ # Express API + EJS views
|
||||||
|
frontend/ # React app (Vite) + Nginx container
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.prod.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
The backend router is mounted at `/default`:
|
||||||
|
|
||||||
|
- `GET /default/users` → returns `{ users: string[] }`
|
||||||
|
- `GET /default/confirm-user?username=<name>` → validates user and returns metadata (e.g. next id)
|
||||||
|
- `POST /default/new-entry?username=<name>` → stores a new entry in MySQL
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Backend environment variables
|
||||||
|
|
||||||
|
The backend reads configuration from environment variables (and can also load a `backend/.env` file via `dotenv`).
|
||||||
|
|
||||||
|
- `PORT` (e.g. `8004`)
|
||||||
|
- `DB_HOST` (e.g. `ca-lose-mysql` when using Docker Compose)
|
||||||
|
- `DB_USER` (e.g. `root`)
|
||||||
|
- `DB_PASSWORD`
|
||||||
|
- `DB_NAME` (e.g. `ca_lose`)
|
||||||
|
|
||||||
|
### Docker Compose environment
|
||||||
|
|
||||||
|
Both compose files expect `DB_PASSWORD` to be present in your shell environment or a root `.env` file.
|
||||||
|
|
||||||
|
Create a root `.env` (not committed) like:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DB_PASSWORD=change-me
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
### Option A: Docker Compose (backend + database)
|
||||||
|
|
||||||
|
This is the quickest way to get the API + MySQL running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
What you get:
|
||||||
|
|
||||||
|
- Backend: http://localhost:8004
|
||||||
|
- MySQL: localhost:3311 (container port 3306)
|
||||||
|
|
||||||
|
Note: In `docker-compose.yml` the `frontend` service is currently commented out.
|
||||||
|
|
||||||
|
### Option B: Run frontend locally (Vite) + run backend & DB
|
||||||
|
|
||||||
|
1. Start DB + backend (Docker):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build database backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start frontend (local Node):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the Vite dev server URL (usually http://localhost:5173).
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- During local development (Vite), the frontend currently calls the backend via a hard-coded URL (`http://localhost:8004`).
|
||||||
|
- When the frontend is served by the Nginx container, requests are expected to go through the Nginx proxy at `/backend/*` (see `frontend/nginx.conf`). In that setup the frontend should use `/backend/default/...` instead of `http://localhost:8004/...`.
|
||||||
|
|
||||||
|
## Database setup
|
||||||
|
|
||||||
|
The schema file is in `backend/scheme.sql`.
|
||||||
|
|
||||||
|
- The `users` table is required for the user dropdown.
|
||||||
|
- The backend creates an entry table dynamically per user and date when calling `GET /default/confirm-user`.
|
||||||
|
|
||||||
|
Example (inside MySQL):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO users (username, first_name, last_name)
|
||||||
|
VALUES ('demo', 'Demo', 'User');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production / Deployment
|
||||||
|
|
||||||
|
The production compose file builds and runs:
|
||||||
|
|
||||||
|
- `frontend` (Nginx)
|
||||||
|
- `backend` (Express)
|
||||||
|
- `database` (MySQL)
|
||||||
|
- optional infrastructure: `dnsmasq` and `wireguard` (wg-easy)
|
||||||
|
|
||||||
|
Start it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `docker-compose.prod.yml` uses an **external** network called `proxynet`. Create it if it does not exist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create proxynet
|
||||||
|
```
|
||||||
|
|
||||||
|
- The production file assigns static IPs; adjust the subnets/IPs if they conflict with your environment.
|
||||||
|
|
||||||
|
## Lint / build
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Backend does not start / ESM imports**: The backend code uses ESM `import` syntax. Node must treat the backend as an ESM project (commonly by adding `"type": "module"` to `backend/package.json`). Without that, the backend will fail to start (including in Docker).
|
||||||
|
- **MySQL timezone mount**: The compose files mount `./mysql-timezone.cnf` to `/etc/mysql/conf.d/timezone.cnf`. Ensure `mysql-timezone.cnf` is a file (not a directory) with valid MySQL config, or remove/adjust the mount.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the Apache License 2.0. See [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const confirmUser = async (username) => {
|
|||||||
console.log(tableName);
|
console.log(tableName);
|
||||||
|
|
||||||
const [createTable] = await pool.query(
|
const [createTable] = await pool.query(
|
||||||
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
`CREATE TABLE IF NOT EXISTS ?? (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
Vorname VARCHAR(100) NOT NULL,
|
Vorname VARCHAR(100) NOT NULL,
|
||||||
Nachname Varchar(100) NOT NULL,
|
Nachname Varchar(100) NOT NULL,
|
||||||
@@ -56,14 +56,16 @@ export const confirmUser = async (username) => {
|
|||||||
Plz_Ort Varchar(100),
|
Plz_Ort Varchar(100),
|
||||||
Zahlungsmethode Varchar(100),
|
Zahlungsmethode Varchar(100),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)`
|
)`,
|
||||||
|
[tableName],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (createTable) {
|
if (createTable) {
|
||||||
let nextID;
|
let nextID;
|
||||||
const getNextID = async () => {
|
const getNextID = async () => {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT id FROM ${tableName} ORDER BY id DESC LIMIT 1`
|
`SELECT id FROM ?? ORDER BY id DESC LIMIT 1`,
|
||||||
|
[tableName],
|
||||||
);
|
);
|
||||||
nextID = rows.length > 0 ? rows[0].id + 1 : 1;
|
nextID = rows.length > 0 ? rows[0].id + 1 : 1;
|
||||||
};
|
};
|
||||||
@@ -87,8 +89,9 @@ export const newEntry = async (formData, username) => {
|
|||||||
const tableName = confirmation.tableName;
|
const tableName = confirmation.tableName;
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO ${tableName} (Vorname, Nachname, EMail, Telefonnummer, Lose, Firmenname, Vorname_Geschaeftlich, Nachname_Geschaeftlich, EMail_Geschaeftlich, Telefonnummer_Geschaeftlich, Strasse_Hausnr, Plz_Ort, Zahlungsmethode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT INTO ?? (Vorname, Nachname, EMail, Telefonnummer, Lose, Firmenname, Vorname_Geschaeftlich, Nachname_Geschaeftlich, EMail_Geschaeftlich, Telefonnummer_Geschaeftlich, Strasse_Hausnr, Plz_Ort, Zahlungsmethode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
|
tableName,
|
||||||
formData.firstName,
|
formData.firstName,
|
||||||
formData.lastName,
|
formData.lastName,
|
||||||
formData.email,
|
formData.email,
|
||||||
@@ -102,7 +105,7 @@ export const newEntry = async (formData, username) => {
|
|||||||
formData.street,
|
formData.street,
|
||||||
formData.postalCode,
|
formData.postalCode,
|
||||||
formData.paymentMethod,
|
formData.paymentMethod,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true, insertId: result.insertId };
|
return { success: true, insertId: result.insertId };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ CREATE TABLE users (
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* This scheme does not have to be implemented manually. It always will be generated by the backend */
|
||||||
CREATE TABLE xx_DD_MM_YYYY (
|
CREATE TABLE xx_DD_MM_YYYY (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
Vorname VARCHAR(100) NOT NULL,
|
Vorname VARCHAR(100) NOT NULL,
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
services:
|
|
||||||
frontend:
|
|
||||||
container_name: ca-lose-frontend
|
|
||||||
hostname: lose-verkaufen
|
|
||||||
build: ./frontend
|
|
||||||
networks:
|
|
||||||
ca-lose-internal:
|
|
||||||
ipv4_address: 172.25.0.2
|
|
||||||
proxynet:
|
|
||||||
ipv4_address: 172.20.0.61
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
backend:
|
|
||||||
container_name: ca-lose-backend
|
|
||||||
hostname: backend
|
|
||||||
build: ./backend
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
DB_HOST: ca-lose-mysql
|
|
||||||
DB_USER: root
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
|
||||||
DB_NAME: ca_lose
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
networks:
|
|
||||||
ca-lose-internal:
|
|
||||||
ipv4_address: 172.25.0.3
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
database:
|
|
||||||
container_name: ca-lose-mysql
|
|
||||||
hostname: database
|
|
||||||
image: mysql:8.0
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
|
||||||
MYSQL_DATABASE: ca_lose
|
|
||||||
TZ: Europe/Berlin
|
|
||||||
volumes:
|
|
||||||
- ca-lose_mysql:/var/lib/mysql
|
|
||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
|
||||||
networks:
|
|
||||||
ca-lose-internal:
|
|
||||||
ipv4_address: 172.25.0.4
|
|
||||||
proxynet:
|
|
||||||
ipv4_address: 172.20.0.60
|
|
||||||
|
|
||||||
# DNS Server für Hostname-Auflösung innerhalb des VPN
|
|
||||||
dnsmasq:
|
|
||||||
container_name: ca-lose-dns
|
|
||||||
image: andyshinn/dnsmasq:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
command: >
|
|
||||||
--no-daemon
|
|
||||||
--log-queries
|
|
||||||
--address=/lose-verkaufen/172.25.0.2
|
|
||||||
--address=/frontend/172.25.0.2
|
|
||||||
--address=/backend/172.25.0.3
|
|
||||||
--address=/database/172.25.0.4
|
|
||||||
--address=/wg-admin/172.25.0.10
|
|
||||||
networks:
|
|
||||||
ca-lose-internal:
|
|
||||||
ipv4_address: 172.25.0.53
|
|
||||||
|
|
||||||
# WireGuard VPN mit Web-UI (wg-easy)
|
|
||||||
wireguard:
|
|
||||||
image: ghcr.io/wg-easy/wg-easy:latest
|
|
||||||
container_name: ca-lose-wireguard
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
- SYS_MODULE
|
|
||||||
environment:
|
|
||||||
LANG: de
|
|
||||||
WG_HOST: dus3.the1s.de
|
|
||||||
WG_PORT: "51830"
|
|
||||||
PORT: "51821"
|
|
||||||
WG_DEFAULT_ADDRESS: 10.14.14.x
|
|
||||||
WG_DEFAULT_DNS: "172.25.0.53"
|
|
||||||
WG_ALLOWED_IPS: 172.25.0.0/24
|
|
||||||
WG_PERSISTENT_KEEPALIVE: "25"
|
|
||||||
WG_POST_UP: "iptables -t nat -A POSTROUTING -s 10.14.14.0/24 -o eth0 -j MASQUERADE; iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -A FORWARD -i wg0 -d 172.25.0.2 -j ACCEPT; iptables -A FORWARD -i wg0 -d 172.25.0.53 -j ACCEPT; iptables -A FORWARD -i wg0 -j DROP"
|
|
||||||
WG_POST_DOWN: "iptables -t nat -D POSTROUTING -s 10.14.14.0/24 -o eth0 -j MASQUERADE; iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -D FORWARD -i wg0 -d 172.25.0.2 -j ACCEPT; iptables -D FORWARD -i wg0 -d 172.25.0.53 -j ACCEPT; iptables -D FORWARD -i wg0 -j DROP"
|
|
||||||
volumes:
|
|
||||||
- wireguard-data:/etc/wireguard
|
|
||||||
- /lib/modules:/lib/modules:ro
|
|
||||||
ports:
|
|
||||||
- "51830:51830/udp"
|
|
||||||
sysctls:
|
|
||||||
- net.ipv4.ip_forward=1
|
|
||||||
- net.ipv4.conf.all.src_valid_mark=1
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- dnsmasq
|
|
||||||
- frontend
|
|
||||||
networks:
|
|
||||||
ca-lose-internal:
|
|
||||||
ipv4_address: 172.25.0.10
|
|
||||||
proxynet:
|
|
||||||
ipv4_address: 172.20.0.50
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
ca-lose_mysql:
|
|
||||||
wireguard-data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
ca-lose-internal:
|
|
||||||
driver: bridge
|
|
||||||
ipam:
|
|
||||||
config:
|
|
||||||
- subnet: 172.25.0.0/24
|
|
||||||
gateway: 172.25.0.1
|
|
||||||
proxynet:
|
|
||||||
external: true
|
|
||||||
+2
-2
@@ -4,8 +4,8 @@ This template provides a minimal setup to get React working in Vite with HMR and
|
|||||||
|
|
||||||
Currently, two official plugins are available:
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
- [@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
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
- [@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
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
## React Compiler
|
## React Compiler
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export default defineConfig([
|
|||||||
reactRefresh.configs.vite,
|
reactRefresh.configs.vite,
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-2
@@ -2,9 +2,20 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/favicon-light.png"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/favicon-dark.png"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lose verkaufen</title>
|
<title>Lose Verkaufen</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+1165
-1748
File diff suppressed because it is too large
Load Diff
+28
-27
@@ -12,37 +12,38 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@fontsource/roboto": "^5.2.9",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@mui/icons-material": "^7.3.6",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@mui/material": "^7.3.6",
|
"@mui/icons-material": "^9.0.1",
|
||||||
"@mui/styled-engine-sc": "^7.3.6",
|
"@mui/joy": "^5.0.0-beta.52",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@mui/material": "^9.0.1",
|
||||||
"i18next": "^25.7.4",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"i18next": "^26.0.10",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide": "^0.562.0",
|
"lucide-react": "^1.14.0",
|
||||||
"lucide-react": "^0.562.0",
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react": "^19.2.3",
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-i18next": "^17.0.7",
|
||||||
"react-i18next": "^16.5.3",
|
"react-router-dom": "^7.15.0",
|
||||||
"react-router-dom": "^7.11.0",
|
"tailwindcss": "^4.3.0"
|
||||||
"styled-components": "^6.1.19",
|
},
|
||||||
"tailwind-merge": "^3.3.1",
|
"peerDependencies": {
|
||||||
"tailwindcss": "^4.1.11",
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.12.2",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^10.2.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^16.5.0",
|
"globals": "^17.5.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.58.2",
|
||||||
"vite": "^7.2.4"
|
"vite": "^8.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 569 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 532 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"fill" : {
|
||||||
|
"automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000"
|
||||||
|
},
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"blend-mode" : "normal",
|
||||||
|
"fill-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : "automatic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : {
|
||||||
|
"solid" : "extended-srgb:0.00000,0.53333,1.00000,1.00000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image-name" : "Unbenannt.png",
|
||||||
|
"name" : "Unbenannt",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.9,
|
||||||
|
"translation-in-points" : [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
export const API_BASE =
|
||||||
|
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||||
|
import.meta.env.VITE_BACKEND_URL ||
|
||||||
|
"http://localhost:8004";
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export interface FormData {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
tickets: number;
|
||||||
|
companyName: string;
|
||||||
|
cmpFirstName: string;
|
||||||
|
cpmLastName: string;
|
||||||
|
cpmEmail: string;
|
||||||
|
cpmPhoneNumber: string;
|
||||||
|
street: string;
|
||||||
|
postalCode: string;
|
||||||
|
paymentMethod: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
type: "primary" | "neutral" | "danger" | "success" | "warning";
|
||||||
|
headline: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
@@ -1 +1,12 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./utils/i18n/index.ts";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import "./utils/i18n";
|
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
+313
-357
@@ -1,35 +1,40 @@
|
|||||||
import {
|
|
||||||
TextField,
|
|
||||||
FormControlLabel,
|
|
||||||
Checkbox,
|
|
||||||
Button,
|
|
||||||
Alert,
|
|
||||||
CircularProgress,
|
|
||||||
Autocomplete,
|
|
||||||
Chip,
|
|
||||||
Box,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { submitFormData } from "../utils/sender";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
Alert,
|
||||||
|
Typography,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Autocomplete,
|
||||||
|
ButtonGroup,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
ModalClose,
|
||||||
|
} from "@mui/joy";
|
||||||
|
import { submitFormData } from "../utils/sender";
|
||||||
|
import { API_BASE } from "../config/api.config";
|
||||||
|
import type { FormData, Message } from "../config/interfaces.config";
|
||||||
|
import PersonIcon from "@mui/icons-material/Person";
|
||||||
|
import QrCodeIcon from "@mui/icons-material/QrCode";
|
||||||
|
import TranslateIcon from "@mui/icons-material/Translate";
|
||||||
|
import qrCode from "../assets/PayPal-QR-Code.png";
|
||||||
|
|
||||||
interface Message {
|
const PAYMENT_METHODS = ["bar", "paypal", "andere"] as const;
|
||||||
type: "error" | "info" | "success" | "warning";
|
const PAYMENT_LABELS: Record<string, string> = {
|
||||||
headline: string;
|
bar: "Cash",
|
||||||
text: string;
|
paypal: "PayPal",
|
||||||
}
|
andere: "Transfer",
|
||||||
|
};
|
||||||
|
|
||||||
export const MainForm = () => {
|
const DEFAULT_FORM: FormData = {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [invoice, setInvoice] = useState(false);
|
|
||||||
const [msg, setMsg] = useState<Message | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [nextID, setNextID] = useState<number | null>(null);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
@@ -43,59 +48,120 @@ export const MainForm = () => {
|
|||||||
street: "",
|
street: "",
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
paymentMethod: "",
|
paymentMethod: "",
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// ─── Field component lives OUTSIDE MainForm so React doesn't treat it as a
|
||||||
|
// new component type on every render, which would cause inputs to lose focus.
|
||||||
|
const Field = ({
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
type = "text",
|
||||||
|
required = true,
|
||||||
|
formData,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
name: keyof FormData;
|
||||||
|
type?: string;
|
||||||
|
required?: boolean;
|
||||||
|
formData: FormData;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}) => (
|
||||||
|
<FormControl required={required}>
|
||||||
|
<FormLabel>{label}</FormLabel>
|
||||||
|
<Input
|
||||||
|
name={name}
|
||||||
|
type={type}
|
||||||
|
value={formData[name] as string}
|
||||||
|
onChange={onChange}
|
||||||
|
variant="soft"
|
||||||
|
sx={{ borderRadius: "10px" }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MainForm = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const [invoice, setInvoice] = useState(false);
|
||||||
|
const [msg, setMsg] = useState<Message | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [nextID, setNextID] = useState<number | null>(null);
|
||||||
const [users, setUsers] = useState<string[]>([]);
|
const [users, setUsers] = useState<string[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||||
|
const [formData, setFormData] = useState<FormData>(DEFAULT_FORM);
|
||||||
useEffect(() => {
|
const [showSelectUser, setShowSelectUser] = useState(false);
|
||||||
// Fetch user data or any other data needed for the form
|
const [QRmodal, setQRmodal] = useState(false);
|
||||||
try {
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
const response = await fetch("http://localhost:8004/default/users");
|
|
||||||
const data = await response.json();
|
|
||||||
setUsers(data.users);
|
|
||||||
};
|
|
||||||
fetchUsers();
|
|
||||||
console.log(users);
|
|
||||||
} catch (error) {
|
|
||||||
setMsg({
|
|
||||||
type: "error",
|
|
||||||
headline: t("error"),
|
|
||||||
text: t("failed-to-load-users"),
|
|
||||||
});
|
|
||||||
console.error("Error fetching users:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Cookies.get("selectedUser")) {
|
|
||||||
const cookieUser = Cookies.get("selectedUser")!;
|
|
||||||
setSelectedUser(cookieUser);
|
|
||||||
confirmUser(cookieUser);
|
|
||||||
}
|
|
||||||
}, [isLoading]);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmUser = async (selectedUser: string) => {
|
const confirmUser = async (username: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:8004/default/confirm-user?username=${selectedUser}`,
|
`${API_BASE}/default/confirm-user?username=${username}`,
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await res.json();
|
||||||
setNextID(data.nextID);
|
setNextID(data.nextID);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error confirming user:", error);
|
console.error("Error confirming user:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserSelection = (selectedUser: string | null) => {
|
const handleUserSelection = (username: string | null) => {
|
||||||
if (!selectedUser) return;
|
if (!username) return;
|
||||||
setSelectedUser(selectedUser);
|
setSelectedUser(username);
|
||||||
confirmUser(selectedUser);
|
confirmUser(username);
|
||||||
Cookies.set("selectedUser", selectedUser);
|
Cookies.set("selectedUser", username);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeTranslation = () => {
|
||||||
|
const clientLng = i18n.language;
|
||||||
|
|
||||||
|
if (clientLng === "en") {
|
||||||
|
i18n.changeLanguage("de");
|
||||||
|
Cookies.set("language", "de");
|
||||||
|
} else if (clientLng === "de") {
|
||||||
|
i18n.changeLanguage("en");
|
||||||
|
Cookies.set("language", "en");
|
||||||
|
} else {
|
||||||
|
setMsg({
|
||||||
|
type: "danger",
|
||||||
|
headline: "Error",
|
||||||
|
text: "Cannot change langugage.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData.paymentMethod === "paypal") {
|
||||||
|
setQRmodal(true);
|
||||||
|
}
|
||||||
|
}, [formData.paymentMethod]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/default/users`);
|
||||||
|
const data = await res.json();
|
||||||
|
setUsers(data.users);
|
||||||
|
} catch {
|
||||||
|
setMsg({
|
||||||
|
type: "danger",
|
||||||
|
headline: t("error"),
|
||||||
|
text: t("failed-to-load-users"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const cookieUser = Cookies.get("selectedUser");
|
||||||
|
if (cookieUser) {
|
||||||
|
setSelectedUser(cookieUser);
|
||||||
|
confirmUser(cookieUser);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -104,7 +170,7 @@ export const MainForm = () => {
|
|||||||
document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`;
|
document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`;
|
||||||
} else {
|
} else {
|
||||||
setMsg({
|
setMsg({
|
||||||
type: "error",
|
type: "danger",
|
||||||
headline: t("error"),
|
headline: t("error"),
|
||||||
text: result.error || t("form-submission-failed"),
|
text: result.error || t("form-submission-failed"),
|
||||||
});
|
});
|
||||||
@@ -114,35 +180,78 @@ export const MainForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Shorthand so we don't repeat formData + onChange on every Field usage
|
||||||
|
const fieldProps = { formData, onChange: handleChange };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
className="bg-gray-100 py-10 px-4"
|
<Modal open={showSelectUser}>
|
||||||
sx={{
|
<ModalDialog color="primary" layout="center" size="lg">
|
||||||
minHeight: "100vh",
|
<ModalClose onClick={() => setShowSelectUser(false)} />
|
||||||
|
<Typography>{t("user")}</Typography>
|
||||||
|
{/* User selection */}
|
||||||
|
<Autocomplete
|
||||||
|
options={users}
|
||||||
|
value={selectedUser}
|
||||||
|
onChange={(_, value) => handleUserSelection(value)}
|
||||||
|
placeholder={t("user")}
|
||||||
|
variant="soft"
|
||||||
|
sx={{ borderRadius: "10px" }}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") e.preventDefault();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
<Modal open={QRmodal}>
|
||||||
|
<ModalDialog color="primary" layout="center" size="lg">
|
||||||
|
<ModalClose onClick={() => setQRmodal(false)} />
|
||||||
|
<Typography>{t("qr-text")}</Typography>
|
||||||
|
<img
|
||||||
|
src={qrCode}
|
||||||
|
alt="PayPal QR Code"
|
||||||
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
height: "auto",
|
||||||
alignItems: "center",
|
maxHeight: "70vh",
|
||||||
justifyContent: "center",
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div className="min-h-screen w-full flex items-center justify-center from-slate-100 to-blue-50 p-4">
|
||||||
|
<Sheet
|
||||||
|
variant="plain"
|
||||||
|
className="w-full"
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
maxWidth: 460,
|
||||||
|
borderRadius: "24px",
|
||||||
|
p: { xs: "1.5rem", sm: "2rem" },
|
||||||
|
boxShadow: "0 24px 64px -12px rgba(0,0,0,0.18)",
|
||||||
|
background: "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Paper
|
<ButtonGroup
|
||||||
elevation={6}
|
color="primary"
|
||||||
className="w-full rounded-2xl"
|
disabled={false}
|
||||||
sx={{
|
size="lg"
|
||||||
backgroundColor: "#fff",
|
spacing={1}
|
||||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
|
variant="soft"
|
||||||
width: "100%",
|
|
||||||
maxWidth: {
|
|
||||||
xs: 360, // kompakte Handy-Ansicht
|
|
||||||
sm: 420, // kleine Tablets / große Phones
|
|
||||||
md: 480, // Desktop, bleibt angenehm schmal
|
|
||||||
},
|
|
||||||
padding: {
|
|
||||||
xs: "1.5rem",
|
|
||||||
sm: "2rem",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<IconButton onClick={() => setShowSelectUser(true)}>
|
||||||
|
<PersonIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={() => setQRmodal(true)}>
|
||||||
|
<QrCodeIcon />
|
||||||
|
</IconButton>
|
||||||
|
{/* Language toggle */}
|
||||||
|
<IconButton onClick={changeTranslation}>
|
||||||
|
<TranslateIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -150,337 +259,184 @@ export const MainForm = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
{/* User Selection */}
|
{/* Next ID badge */}
|
||||||
<Autocomplete
|
|
||||||
disablePortal
|
|
||||||
options={users}
|
|
||||||
value={selectedUser}
|
|
||||||
fullWidth
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label={t("user")} variant="filled" />
|
|
||||||
)}
|
|
||||||
onChange={(_event, value) => handleUserSelection(value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.defaultMuiPrevented = true;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Next ID Chip */}
|
|
||||||
<Chip
|
<Chip
|
||||||
label={`#${nextID ?? "N/A"}`}
|
size="lg"
|
||||||
color="primary"
|
variant="solid"
|
||||||
|
color="neutral"
|
||||||
sx={{
|
sx={{
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
mt: 0.5,
|
|
||||||
mb: 0.5,
|
|
||||||
py: 0.5,
|
|
||||||
px: 1.25,
|
|
||||||
borderRadius: "999px",
|
borderRadius: "999px",
|
||||||
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
|
fontWeight: 600,
|
||||||
|
marginTop: 1,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
#{nextID ?? "N/A"}
|
||||||
|
</Chip>
|
||||||
|
|
||||||
{/* Name Fields - Two Columns */}
|
{/* Name row */}
|
||||||
<Box className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<TextField
|
<Field label={t("first-name")} name="firstName" {...fieldProps} />
|
||||||
required
|
<Field label={t("last-name")} name="lastName" {...fieldProps} />
|
||||||
id="first-name"
|
</div>
|
||||||
label={t("first-name")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.firstName}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="firstName"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="last-name"
|
|
||||||
label={t("last-name")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.lastName}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="lastName"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Email */}
|
<Field
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="email"
|
|
||||||
label={t("email")}
|
label={t("email")}
|
||||||
variant="filled"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="email"
|
name="email"
|
||||||
fullWidth
|
type="email"
|
||||||
|
{...fieldProps}
|
||||||
/>
|
/>
|
||||||
|
<Field
|
||||||
{/* Phone Number */}
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="phone-number"
|
|
||||||
label={t("phone-number")}
|
label={t("phone-number")}
|
||||||
variant="filled"
|
|
||||||
type="tel"
|
|
||||||
value={formData.phoneNumber}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="phoneNumber"
|
name="phoneNumber"
|
||||||
fullWidth
|
type="tel"
|
||||||
|
{...fieldProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tickets and Invoice Checkbox */}
|
{/* Tickets + Invoice toggle */}
|
||||||
<Box className="grid grid-cols-2 gap-3 items-center">
|
<div className="grid grid-cols-2 gap-3 items-end">
|
||||||
<TextField
|
<FormControl required>
|
||||||
required
|
<FormLabel>{t("tickets")}</FormLabel>
|
||||||
id="tickets"
|
<Input
|
||||||
|
name="tickets"
|
||||||
type="number"
|
type="number"
|
||||||
label={t("tickets")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.tickets}
|
value={formData.tickets}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
name="tickets"
|
slotProps={{ input: { min: 1 } }}
|
||||||
fullWidth
|
variant="soft"
|
||||||
inputProps={{ min: 1 }}
|
sx={{ borderRadius: "10px" }}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
</FormControl>
|
||||||
control={
|
<div className="flex items-center pb-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={invoice}
|
checked={invoice}
|
||||||
onChange={(e) => setInvoice(e.target.checked)}
|
onChange={(e) => setInvoice(e.target.checked)}
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={t("invoice")}
|
label={t("invoice")}
|
||||||
className="justify-end"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Invoice Fields */}
|
{/* Invoice details (conditional) */}
|
||||||
{invoice && (
|
{invoice && (
|
||||||
<Box
|
<div className="flex flex-col gap-3 pt-4 border-t border-blue-200">
|
||||||
className="flex flex-col gap-2 pt-3 mt-2"
|
<Typography level="title-sm" color="primary">
|
||||||
sx={{
|
{t("invoice-details")}
|
||||||
borderTop: "2px solid",
|
|
||||||
borderColor: "primary.light",
|
|
||||||
borderRadius: "0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="company-name"
|
|
||||||
label={t("company-name")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.companyName}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="companyName"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Invoice Name Fields - Two Columns */}
|
|
||||||
<Box className="grid grid-cols-2 gap-3">
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="first-name_invoice"
|
|
||||||
label={t("first-name")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.cmpFirstName}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="cmpFirstName"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="last-name_invoice"
|
|
||||||
label={t("last-name")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.cpmLastName}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="cpmLastName"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="street"
|
|
||||||
label={t("street")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.street}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="street"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="postal-code"
|
|
||||||
label={t("postal-code")}
|
|
||||||
variant="filled"
|
|
||||||
value={formData.postalCode}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="postalCode"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="phone-number_invoice"
|
|
||||||
label={t("phone-number")}
|
|
||||||
variant="filled"
|
|
||||||
type="tel"
|
|
||||||
value={formData.cpmPhoneNumber}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="cpmPhoneNumber"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
id="email_invoice"
|
|
||||||
label={t("email")}
|
|
||||||
variant="filled"
|
|
||||||
type="email"
|
|
||||||
value={formData.cpmEmail}
|
|
||||||
onChange={handleChange}
|
|
||||||
name="cpmEmail"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{/* Payment Methods */}
|
|
||||||
<Box className="flex flex-col gap-2 mt-2">
|
|
||||||
<Typography
|
|
||||||
component="label"
|
|
||||||
sx={{
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "text.secondary",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("select-payment-method")} *
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box className="flex gap-2 flex-wrap">
|
<Field
|
||||||
|
label={t("company-name")}
|
||||||
|
name="companyName"
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Field
|
||||||
|
label={t("first-name")}
|
||||||
|
name="cmpFirstName"
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={t("last-name")}
|
||||||
|
name="cpmLastName"
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Field label={t("street")} name="street" {...fieldProps} />
|
||||||
|
<Field
|
||||||
|
label={t("postal-code")}
|
||||||
|
name="postalCode"
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={t("phone-number")}
|
||||||
|
name="cpmPhoneNumber"
|
||||||
|
type="tel"
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={t("email")}
|
||||||
|
name="cpmEmail"
|
||||||
|
type="email"
|
||||||
|
{...fieldProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Payment method selection */}
|
||||||
|
<FormControl required>
|
||||||
|
<FormLabel>{t("select-payment-method")}</FormLabel>
|
||||||
|
<div className="flex gap-2 flex-wrap mt-1">
|
||||||
|
{PAYMENT_METHODS.map((method) => (
|
||||||
<Button
|
<Button
|
||||||
|
key={method}
|
||||||
variant={
|
variant={
|
||||||
formData.paymentMethod === "bar" ? "contained" : "outlined"
|
formData.paymentMethod === method ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
|
color="primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setFormData({ ...formData, paymentMethod: "bar" })
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
paymentMethod: method,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: "100px",
|
minWidth: "90px",
|
||||||
py: 1.5,
|
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
|
py: 1.5,
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
fontWeight: formData.paymentMethod === "bar" ? 600 : 400,
|
fontWeight: formData.paymentMethod === method ? 700 : 400,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("cash")}
|
{PAYMENT_LABELS[method]}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
))}
|
||||||
variant={
|
</div>
|
||||||
formData.paymentMethod === "paypal" ? "contained" : "outlined"
|
{/* Hidden required input to enforce payment selection on submit */}
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setFormData({ ...formData, paymentMethod: "paypal" })
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
minWidth: "100px",
|
|
||||||
py: 1.5,
|
|
||||||
borderRadius: "12px",
|
|
||||||
textTransform: "none",
|
|
||||||
fontWeight: formData.paymentMethod === "paypal" ? 600 : 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("paypal")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
formData.paymentMethod === "andere" ? "contained" : "outlined"
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setFormData({ ...formData, paymentMethod: "andere" })
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
minWidth: "100px",
|
|
||||||
py: 1.5,
|
|
||||||
borderRadius: "12px",
|
|
||||||
textTransform: "none",
|
|
||||||
fontWeight: formData.paymentMethod === "andere" ? 600 : 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("transfer")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
{!formData.paymentMethod && (
|
{!formData.paymentMethod && (
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
autoComplete="off"
|
required
|
||||||
|
value=""
|
||||||
|
onChange={() => {}}
|
||||||
style={{
|
style={{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
}}
|
}}
|
||||||
required
|
|
||||||
value={formData.paymentMethod}
|
|
||||||
onChange={() => {}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</FormControl>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit button */}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
loading={isLoading}
|
||||||
disabled={isLoading || !formData.paymentMethod}
|
disabled={!formData.paymentMethod}
|
||||||
fullWidth
|
size="lg"
|
||||||
size="large"
|
|
||||||
sx={{
|
sx={{
|
||||||
mt: 3,
|
mt: 2,
|
||||||
py: 2,
|
borderRadius: "14px",
|
||||||
textTransform: "uppercase",
|
fontWeight: 700,
|
||||||
fontWeight: "bold",
|
letterSpacing: "0.05em",
|
||||||
borderRadius: "12px",
|
background: "linear-gradient(135deg, #2563eb, #1d4ed8)",
|
||||||
fontSize: "1rem",
|
|
||||||
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
|
|
||||||
boxShadow: "0 4px 14px 0 rgba(25, 118, 210, 0.39)",
|
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
background: "linear-gradient(135deg, #1565c0 0%, #0d47a1 100%)",
|
background: "linear-gradient(135deg, #1d4ed8, #1e40af)",
|
||||||
boxShadow: "0 6px 20px 0 rgba(25, 118, 210, 0.5)",
|
|
||||||
},
|
|
||||||
"&:disabled": {
|
|
||||||
background: "#e0e0e0",
|
|
||||||
boxShadow: "none",
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{t("submit")}
|
||||||
<CircularProgress size={24} color="inherit" />
|
|
||||||
) : (
|
|
||||||
t("submit")
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Alert Message */}
|
{/* Alert message */}
|
||||||
{msg && (
|
{msg && (
|
||||||
<Alert severity={msg.type} sx={{ mt: 2 }}>
|
<Alert color={msg.type} sx={{ borderRadius: "12px" }}>
|
||||||
{msg.headline}: {msg.text}
|
<strong>{msg.headline}:</strong> {msg.text}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Sheet>
|
||||||
</Box>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+137
-172
@@ -1,23 +1,20 @@
|
|||||||
import { Box, Paper, Typography, Chip, Button } from "@mui/material";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CircleCheck } from "lucide-react";
|
import { CircleCheck } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Sheet, Typography, Chip, Button } from "@mui/joy";
|
||||||
|
|
||||||
export const SuccessPage = () => {
|
export const SuccessPage = () => {
|
||||||
const [orderId, setOrderId] = useState<string | null>(null);
|
|
||||||
const [tickets, setNumberOfTickets] = useState<number>(0);
|
|
||||||
const [animate, setAnimate] = useState(false);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [orderId, setOrderId] = useState<string | null>(null);
|
||||||
|
const [tickets, setTickets] = useState<number>(0);
|
||||||
|
const [animate, setAnimate] = useState(false);
|
||||||
const [seconds, setSeconds] = useState(30);
|
const [seconds, setSeconds] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const id = params.get("id");
|
setOrderId(params.get("id"));
|
||||||
const numberOfTickets = params.get("tickets");
|
setTickets(parseInt(params.get("tickets") ?? "0", 10));
|
||||||
|
// Small delay so the CSS transition actually plays
|
||||||
setOrderId(id);
|
|
||||||
setNumberOfTickets(numberOfTickets ? parseInt(numberOfTickets, 10) : 0);
|
|
||||||
|
|
||||||
setTimeout(() => setAnimate(true), 100);
|
setTimeout(() => setAnimate(true), 100);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -26,185 +23,153 @@ export const SuccessPage = () => {
|
|||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const timer = setTimeout(() => setSeconds((s) => s - 1), 1000);
|
||||||
const timer = setTimeout(() => setSeconds(seconds - 1), 1000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [seconds]);
|
}, [seconds]);
|
||||||
|
|
||||||
|
// Returns a style object that slides the element up + fades it in.
|
||||||
|
// Each section gets a slightly later delay for a staggered entrance.
|
||||||
|
const fadeUp = (delay: string): React.CSSProperties => ({
|
||||||
|
transition: `opacity 0.5s ease-in-out ${delay}, transform 0.5s ease-in-out ${delay}`,
|
||||||
|
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||||
|
opacity: animate ? 1 : 0,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4">
|
<div className="min-h-screen w-full flex items-center justify-center bg-linear-to-br from-slate-800 to-slate-900 p-4">
|
||||||
<Paper
|
<Sheet
|
||||||
elevation={3}
|
variant="plain"
|
||||||
className="w-full max-w-md p-8 rounded-lg"
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#fff",
|
|
||||||
textAlign: "center",
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 440,
|
||||||
|
borderRadius: "24px",
|
||||||
|
p: { xs: "2rem", sm: "2.5rem" },
|
||||||
|
boxShadow: "0 24px 64px -12px rgba(0,0,0,0.4)",
|
||||||
|
background: "#fff",
|
||||||
|
textAlign: "center",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Animated Success Icon */}
|
{/* Green accent bar at the top */}
|
||||||
<Box
|
<div
|
||||||
sx={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
mb: 3,
|
|
||||||
transition: "all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
||||||
transform: animate ? "scale(1)" : "scale(0)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircleCheck size={80} className="text-green-500" strokeWidth={2.5} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Success Message */}
|
|
||||||
<Typography
|
|
||||||
variant="h4"
|
|
||||||
component="h1"
|
|
||||||
gutterBottom
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: "#2e7d32",
|
|
||||||
mb: 2,
|
|
||||||
transition: "all 0.5s ease-in-out 0.2s",
|
|
||||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("form-submitted-successfully")}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
color: "#666",
|
|
||||||
mb: 3,
|
|
||||||
transition: "all 0.5s ease-in-out 0.3s",
|
|
||||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("ticket-payment", { count: tickets })}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Tickets Display */}
|
|
||||||
{tickets > 0 && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mb: 2,
|
|
||||||
transition: "all 0.5s ease-in-out 0.35s",
|
|
||||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Chip
|
|
||||||
label={`${tickets} ${tickets === 1 ? t("ticket") : t("tickets")}`}
|
|
||||||
color="secondary"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: "1rem",
|
|
||||||
py: 2.5,
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Order ID Display */}
|
|
||||||
{orderId && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mb: 3,
|
|
||||||
transition: "all 0.5s ease-in-out 0.4s",
|
|
||||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: "#888",
|
|
||||||
mb: 1,
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("entry-id")}
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label={`#${orderId}`}
|
|
||||||
color="primary"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: "1.25rem",
|
|
||||||
py: 3,
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Return button */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mb: 3,
|
|
||||||
transition: "all 0.5s ease-in-out 0.4s",
|
|
||||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
href="/"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: "1.25rem",
|
|
||||||
py: 3,
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{seconds + " " + t("return-to-homepage")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Additional Info */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: 4,
|
|
||||||
pt: 3,
|
|
||||||
borderTop: "1px solid #e0e0e0",
|
|
||||||
transition: "all 0.5s ease-in-out 0.5s",
|
|
||||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
|
||||||
opacity: animate ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: "#666",
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("thank-you")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Decorative Elements */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: "4px",
|
height: "4px",
|
||||||
background: "linear-gradient(90deg, #4caf50 0%, #81c784 100%)",
|
background: "linear-gradient(90deg, #22c55e, #86efac)",
|
||||||
transition: "all 0.8s ease-in-out 0.6s",
|
transition: "transform 0.8s ease-in-out 0.6s",
|
||||||
transform: animate ? "scaleX(1)" : "scaleX(0)",
|
transform: animate ? "scaleX(1)" : "scaleX(0)",
|
||||||
transformOrigin: "left",
|
transformOrigin: "left",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
|
||||||
</Box>
|
{/* Animated success icon */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: "1.5rem",
|
||||||
|
transition: "all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
||||||
|
transform: animate ? "scale(1)" : "scale(0)",
|
||||||
|
opacity: animate ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircleCheck size={80} color="#22c55e" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Headline */}
|
||||||
|
<div style={fadeUp("0.2s")}>
|
||||||
|
<Typography
|
||||||
|
level="h3"
|
||||||
|
sx={{ fontWeight: 700, color: "#15803d", mb: 1 }}
|
||||||
|
>
|
||||||
|
{t("form-submitted-successfully")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<div style={fadeUp("0.3s")}>
|
||||||
|
<Typography level="body-md" sx={{ color: "#6b7280", mb: 3 }}>
|
||||||
|
{t("ticket-payment", { count: tickets })}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tickets chip */}
|
||||||
|
{tickets > 0 && (
|
||||||
|
<div style={fadeUp("0.35s")} className="flex justify-center mb-3">
|
||||||
|
<Chip
|
||||||
|
size="lg"
|
||||||
|
variant="soft"
|
||||||
|
color="success"
|
||||||
|
sx={{ fontWeight: 700, fontSize: "1rem", px: 2, py: 1 }}
|
||||||
|
>
|
||||||
|
{tickets} {tickets === 1 ? t("ticket") : t("tickets")}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Order ID chip */}
|
||||||
|
{orderId && (
|
||||||
|
<div
|
||||||
|
style={fadeUp("0.4s")}
|
||||||
|
className="flex flex-col items-center gap-1 mb-4"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
level="body-xs"
|
||||||
|
sx={{
|
||||||
|
color: "#9ca3af",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("entry-id")}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="lg"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontWeight: 700, fontSize: "1.25rem", px: 3, py: 1.5 }}
|
||||||
|
>
|
||||||
|
#{orderId}
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Return button with countdown */}
|
||||||
|
<div style={fadeUp("0.45s")} className="mb-4">
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href="/"
|
||||||
|
size="lg"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
borderRadius: "12px",
|
||||||
|
fontWeight: 700,
|
||||||
|
background: "linear-gradient(135deg, #2563eb, #1d4ed8)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #1d4ed8, #1e40af)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{seconds}s — {t("return-to-homepage")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thank-you note */}
|
||||||
|
<div style={fadeUp("0.5s")} className="pt-4 border-t border-slate-100">
|
||||||
|
<Typography
|
||||||
|
level="body-sm"
|
||||||
|
sx={{ color: "#9ca3af", lineHeight: 1.6 }}
|
||||||
|
>
|
||||||
|
{t("thank-you")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"phone-number": "Telefonnummer",
|
"phone-number": "Telefonnummer",
|
||||||
"tickets": "Lose",
|
"tickets": "Lose",
|
||||||
"invoice": "Rechnung",
|
"invoice": "Rechnung",
|
||||||
|
"invoice-details": "Rechnungsdetails",
|
||||||
"company-name": "Firmenname",
|
"company-name": "Firmenname",
|
||||||
"street": "Straße + Haus Nr.",
|
"street": "Straße + Haus Nr.",
|
||||||
"postal-code": "Plz + Stadt",
|
"postal-code": "Plz + Stadt",
|
||||||
@@ -24,5 +25,6 @@
|
|||||||
"entry-id": "Eintrags-ID",
|
"entry-id": "Eintrags-ID",
|
||||||
"thank-you": "Vielen Dank für Ihre Unterstützung der Claudius Akademie! Wir wünschen Ihnen viel Glück mit dem Los.",
|
"thank-you": "Vielen Dank für Ihre Unterstützung der Claudius Akademie! Wir wünschen Ihnen viel Glück mit dem Los.",
|
||||||
"select-payment-method": "Zahlungsmethode auswählen",
|
"select-payment-method": "Zahlungsmethode auswählen",
|
||||||
"return-to-homepage": "Zurück"
|
"return-to-homepage": "Zurück",
|
||||||
|
"qr-text": "PayPal QR-Code der Claudius Akademie"
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"last-name": "Last Name",
|
"last-name": "Last Name",
|
||||||
"phone-number": "Phone Number",
|
"phone-number": "Phone Number",
|
||||||
"invoice": "Invoice",
|
"invoice": "Invoice",
|
||||||
|
"invoice-details": "Invoice Details",
|
||||||
"company-name": "Company Name",
|
"company-name": "Company Name",
|
||||||
"street": "Street + House No.",
|
"street": "Street + House No.",
|
||||||
"postal-code": "Postal Code + City",
|
"postal-code": "Postal Code + City",
|
||||||
@@ -25,5 +26,6 @@
|
|||||||
"entry-id": "Entry ID",
|
"entry-id": "Entry ID",
|
||||||
"thank-you": "Thank you for supporting the Claudius Akademie! We wish you the best of luck with your ticket.",
|
"thank-you": "Thank you for supporting the Claudius Akademie! We wish you the best of luck with your ticket.",
|
||||||
"select-payment-method": "Select Payment Method",
|
"select-payment-method": "Select Payment Method",
|
||||||
"return-to-homepage": "Return"
|
"return-to-homepage": "Return",
|
||||||
|
"qr-text": "PayPal QR-Code from the Claudius Akademie"
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
interface FormData {
|
import { API_BASE } from "../config/api.config";
|
||||||
firstName: string;
|
import type { FormData } from "../config/interfaces.config";
|
||||||
lastName: string;
|
|
||||||
email: string;
|
export const submitFormData = async (
|
||||||
phoneNumber: string;
|
data: FormData,
|
||||||
tickets: number;
|
username: string | null,
|
||||||
companyName: string;
|
) => {
|
||||||
cmpFirstName: string;
|
if (username == null) {
|
||||||
cpmLastName: string;
|
return { success: false, errorCode: "x001" };
|
||||||
cpmEmail: string;
|
}
|
||||||
cpmPhoneNumber: string;
|
|
||||||
street: string;
|
|
||||||
postalCode: string;
|
|
||||||
paymentMethod: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const submitFormData = async (data: FormData, username: string) => {
|
|
||||||
console.log(data);
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://localhost:8004/default/new-entry?username=${username}`,
|
`${API_BASE}/default/new-entry?username=${username}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,jsx,ts,tsx}",
|
|
||||||
// add other paths if needed
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "es2023",
|
||||||
"useDefineForClassFields": true,
|
"lib": ["ES2023", "DOM"],
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"module": "esnext",
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
@@ -17,12 +16,10 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
"target": "ES2023",
|
"target": "es2023",
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"module": "ESNext",
|
"module": "esnext",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
@@ -15,12 +15,10 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [tailwindcss()],
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user