added new frontend and renamed old frontend

This commit is contained in:
2026-05-10 13:30:20 +02:00
parent 75b75169a8
commit e3a26de92d
41 changed files with 6291 additions and 2870 deletions
-19
View File
@@ -1,19 +0,0 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runner
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/dist .
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+2 -2
View File
@@ -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:
- [@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-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](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/)
## React Compiler
-1
View File
@@ -16,7 +16,6 @@ export default defineConfig([
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lose verkaufen</title>
<title>frontend</title>
</head>
<body>
<div id="root"></div>
-26
View File
@@ -1,26 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location = /backend {
return 301 /backend/;
}
location /backend/ {
proxy_pass http://ca-lose-backend:8004/;
}
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
}
+719 -2795
View File
File diff suppressed because it is too large Load Diff
+13 -31
View File
@@ -10,39 +10,21 @@
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.9",
"@mui/icons-material": "^7.3.6",
"@mui/material": "^7.3.6",
"@mui/styled-engine-sc": "^7.3.6",
"@tailwindcss/vite": "^4.1.11",
"i18next": "^25.7.4",
"js-cookie": "^3.0.5",
"lucide": "^0.562.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.0",
"react-i18next": "^16.5.3",
"react-router-dom": "^7.11.0",
"styled-components": "^6.1.19",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tailwindcss-animate": "^1.0.7"
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/js-cookie": "^3.0.6",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.10"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

-1
View File
@@ -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

+184 -1
View File
@@ -1 +1,184 @@
@import "tailwindcss";
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
+117 -12
View File
@@ -1,17 +1,122 @@
import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { MainForm } from "./pages/MainForm";
import { SuccessPage } from "./pages/SuccessPage";
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<MainForm />} />
<Route path="/success" element={<SuccessPage />} />
</Routes>
</BrowserRouter>
);
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
}
export default App;
export default App
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

-4
View File
@@ -1,4 +0,0 @@
export const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8004";
+111 -1
View File
@@ -1 +1,111 @@
@import "tailwindcss";
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
+7 -8
View File
@@ -1,11 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import "./utils/i18n";
import App from "./App.tsx";
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById("root")!).render(
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
</StrictMode>,
)
-514
View File
@@ -1,514 +0,0 @@
import {
TextField,
FormControlLabel,
Checkbox,
Button,
Alert,
CircularProgress,
Autocomplete,
Chip,
Box,
Paper,
Typography,
IconButton,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { submitFormData } from "../utils/sender";
import Cookies from "js-cookie";
import * as React from "react";
import TranslateIcon from "@mui/icons-material/Translate";
import { API_BASE } from "../config/api.config";
interface Message {
type: "error" | "info" | "success" | "warning";
headline: string;
text: string;
}
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 [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
tickets: 1,
companyName: "",
cmpFirstName: "",
cpmLastName: "",
cpmEmail: "",
cpmPhoneNumber: "",
street: "",
postalCode: "",
paymentMethod: "",
});
const [users, setUsers] = useState<string[]>([]);
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const changeTranslation = () => {
const clientLng = i18n.language;
if (clientLng === "en") {
i18n.changeLanguage("de");
} else if (clientLng === "de") {
i18n.changeLanguage("en");
} else {
setMsg({
type: "error",
headline: "Error",
text: "Cannot change langugage.",
});
}
};
useEffect(() => {
// Fetch user data or any other data needed for the form
try {
const fetchUsers = async () => {
const response = await fetch(`${API_BASE}/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>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const confirmUser = async (selectedUser: string) => {
try {
const response = await fetch(
`${API_BASE}/default/confirm-user?username=${selectedUser}`,
);
const data = await response.json();
setNextID(data.nextID);
} catch (error) {
console.error("Error confirming user:", error);
}
};
const handleUserSelection = (selectedUser: string | null) => {
if (!selectedUser) return;
setSelectedUser(selectedUser);
confirmUser(selectedUser);
Cookies.set("selectedUser", selectedUser);
};
const handleSubmit = async () => {
setIsLoading(true);
try {
const result = await submitFormData(formData, selectedUser || "");
if (result.success) {
document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`;
} else {
setMsg({
type: "error",
headline: t("error"),
text: result.error || t("form-submission-failed"),
});
}
} finally {
setIsLoading(false);
}
};
return (
<Box
className="bg-gray-100 py-10 px-4"
sx={{
minHeight: "100vh",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Paper
elevation={6}
className="w-full rounded-2xl"
sx={{
position: "relative",
backgroundColor: "#fff",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
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",
},
}}
>
<Box sx={{ position: "absolute", top: 12, right: 12 }}>
<IconButton
onClick={() => changeTranslation()}
aria-label="translate"
>
<TranslateIcon />
</IconButton>
</Box>
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
className="flex flex-col gap-4"
>
{/* User Selection */}
<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
label={`#${nextID ?? "N/A"}`}
color="primary"
sx={{
alignSelf: "flex-start",
fontWeight: 500,
fontSize: "0.9rem",
mt: 0.5,
mb: 0.5,
py: 0.5,
px: 1.25,
borderRadius: "999px",
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
}}
/>
{/* Name Fields - Two Columns */}
<Box className="grid grid-cols-2 gap-3">
<TextField
required
id="first-name"
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 */}
<TextField
required
id="email"
label={t("email")}
variant="filled"
type="email"
value={formData.email}
onChange={handleChange}
name="email"
fullWidth
/>
{/* Phone Number */}
<TextField
required
id="phone-number"
label={t("phone-number")}
variant="filled"
type="tel"
value={formData.phoneNumber}
onChange={handleChange}
name="phoneNumber"
fullWidth
/>
{/* Tickets and Invoice Checkbox */}
<Box className="grid grid-cols-2 gap-3 items-center">
<TextField
required
id="tickets"
type="number"
label={t("tickets")}
variant="filled"
value={formData.tickets}
onChange={handleChange}
name="tickets"
fullWidth
inputProps={{ min: 1 }}
/>
<FormControlLabel
control={
<Checkbox
checked={invoice}
onChange={(e) => setInvoice(e.target.checked)}
/>
}
label={t("invoice")}
className="justify-end"
/>
</Box>
{/* Invoice Fields */}
{invoice && (
<Box
className="flex flex-col gap-2 pt-3 mt-2"
sx={{
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>
<Box className="flex gap-2 flex-wrap">
<Button
variant={
formData.paymentMethod === "bar" ? "contained" : "outlined"
}
onClick={() =>
setFormData({ ...formData, paymentMethod: "bar" })
}
sx={{
flex: 1,
minWidth: "100px",
py: 1.5,
borderRadius: "12px",
textTransform: "none",
fontWeight: formData.paymentMethod === "bar" ? 600 : 400,
}}
>
{t("cash")}
</Button>
<Button
variant={
formData.paymentMethod === "paypal" ? "contained" : "outlined"
}
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 && (
<input
tabIndex={-1}
autoComplete="off"
style={{
opacity: 0,
width: 0,
height: 0,
position: "absolute",
}}
required
value={formData.paymentMethod}
onChange={() => {}}
/>
)}
</Box>
{/* Submit Button */}
<Button
type="submit"
variant="contained"
disabled={isLoading || !formData.paymentMethod}
fullWidth
size="large"
sx={{
mt: 3,
py: 2,
textTransform: "uppercase",
fontWeight: "bold",
borderRadius: "12px",
fontSize: "1rem",
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
boxShadow: "0 4px 14px 0 rgba(25, 118, 210, 0.39)",
"&:hover": {
background: "linear-gradient(135deg, #1565c0 0%, #0d47a1 100%)",
boxShadow: "0 6px 20px 0 rgba(25, 118, 210, 0.5)",
},
"&:disabled": {
background: "#e0e0e0",
boxShadow: "none",
},
}}
>
{isLoading ? (
<CircularProgress size={24} color="inherit" />
) : (
t("submit")
)}
</Button>
{/* Alert Message */}
{msg && (
<Alert severity={msg.type} sx={{ mt: 2 }}>
{msg.headline}: {msg.text}
</Alert>
)}
</form>
</Paper>
</Box>
);
};
-210
View File
@@ -1,210 +0,0 @@
import { Box, Paper, Typography, Chip, Button } from "@mui/material";
import { useEffect, useState } from "react";
import { CircleCheck } from "lucide-react";
import { useTranslation } from "react-i18next";
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 [seconds, setSeconds] = useState(30);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const id = params.get("id");
const numberOfTickets = params.get("tickets");
setOrderId(id);
setNumberOfTickets(numberOfTickets ? parseInt(numberOfTickets, 10) : 0);
setTimeout(() => setAnimate(true), 100);
}, []);
useEffect(() => {
if (seconds === 0) {
window.location.href = "/";
return;
}
const timer = setTimeout(() => setSeconds(seconds - 1), 1000);
return () => clearTimeout(timer);
}, [seconds]);
return (
<Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4">
<Paper
elevation={3}
className="w-full max-w-md p-8 rounded-lg"
sx={{
backgroundColor: "#fff",
textAlign: "center",
position: "relative",
overflow: "hidden",
}}
>
{/* Animated Success Icon */}
<Box
sx={{
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",
top: 0,
left: 0,
right: 0,
height: "4px",
background: "linear-gradient(90deg, #4caf50 0%, #81c784 100%)",
transition: "all 0.8s ease-in-out 0.6s",
transform: animate ? "scaleX(1)" : "scaleX(0)",
transformOrigin: "left",
}}
/>
</Paper>
</Box>
);
};
-34
View File
@@ -1,34 +0,0 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Cookies from "js-cookie";
import enLang from "./locales/en/en.json";
import deLang from "./locales/de/de.json";
// the translations
// (tip move them in a JSON file and import them,
// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files)
const resources = {
en: {
translation: enLang,
},
de: {
translation: deLang,
},
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
fallbackLng: "en", // use en if detected lng is not available
lng: Cookies.get("language") || navigator.language.split("-")[0] || "en", // Check cookie first, then browser language
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
// if you're using a language detector, do not define the lng option
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;
@@ -1,28 +0,0 @@
{
"first-name": "Vorname",
"last-name": "Nachname",
"phone-number": "Telefonnummer",
"tickets": "Lose",
"invoice": "Rechnung",
"company-name": "Firmenname",
"street": "Straße + Haus Nr.",
"postal-code": "Plz + Stadt",
"email": "E-Mail",
"submit": "Abschicken",
"failed-to-load-users": "Das Laden der Benutzer ist fehlgeschlagen.",
"user": "Benutzer",
"next-id": "Nächste Eintragsnummer: ",
"form-submitted-successfully": "Formular erfolgreich übermittelt!",
"orm-submission-failed": "Formularübermittlung fehlgeschlagen.",
"success": "Erfolg",
"error": "Fehler",
"cash": "Bar",
"paypal": "PayPal",
"transfer": "Andere (notieren)",
"ticket-payment_one": "Sie haben erfolgreich {{count}} Los gekauft.",
"ticket-payment_other": "Sie haben erfolgreich {{count}} Lose gekauft.",
"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.",
"select-payment-method": "Zahlungsmethode auswählen",
"return-to-homepage": "Zurück"
}
@@ -1,29 +0,0 @@
{
"first-name": "First Name",
"last-name": "Last Name",
"phone-number": "Phone Number",
"invoice": "Invoice",
"company-name": "Company Name",
"street": "Street + House No.",
"postal-code": "Postal Code + City",
"email": "Email",
"submit": "Submit Form",
"failed-to-load-users": "Failed to load users.",
"user": "User",
"next-id": "Next Entry Number: ",
"form-submitted-successfully": "Form submitted successfully!",
"orm-submission-failed": "Form submission failed.",
"success": "Success",
"error": "Error",
"cash": "Cash",
"paypal": "PayPal",
"transfer": "Other (note down)",
"ticket-payment_one": "You have successfully purchased {{count}} ticket.",
"ticket-payment_other": "You have successfully purchased {{count}} tickets.",
"ticket": "Ticket",
"tickets": "Tickets",
"entry-id": "Entry ID",
"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",
"return-to-homepage": "Return"
}
-42
View File
@@ -1,42 +0,0 @@
import { API_BASE } from "../config/api.config";
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 const submitFormData = async (data: FormData, username: string) => {
console.log(data);
try {
const response = await fetch(
`${API_BASE}/default/new-entry?username=${username}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
},
);
if (!response.ok) {
const errorText = await response.text();
return { success: false, error: `Server error: ${errorText}` };
}
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
};
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,jsx,ts,tsx}",
// add other paths if needed
],
theme: {
extend: {},
},
plugins: [],
};
+4 -7
View File
@@ -1,10 +1,9 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
@@ -17,12 +16,10 @@
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+3 -5
View File
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"target": "es2023",
"lib": ["ES2023"],
"module": "ESNext",
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
@@ -15,12 +15,10 @@
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}