diff --git a/backend/main.py b/backend/main.py
index 099f0b7..ae5e58c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -253,6 +253,7 @@ from routes.challenges import router as challenges_router
from routes.gassi_zeiten import router as gassi_zeiten_router
from routes.help import router as help_router
from routes.feedback import router as feedback_router
+from routes.contact import router as contact_router
from routes.invoices import router as invoices_router
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
@@ -318,6 +319,7 @@ app.include_router(challenges_router, prefix="/api/challenges", ta
app.include_router(gassi_zeiten_router, prefix="/api/gassi-zeiten", tags=["Gassi-Zeiten"])
app.include_router(help_router, prefix="/api/help", tags=["Hilfe/FAQ"])
app.include_router(feedback_router, prefix="/api/feedback", tags=["Feedback"])
+app.include_router(contact_router, prefix="/api/contact", tags=["Kontakt"])
app.include_router(invoices_router)
@@ -408,7 +410,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.")
return _media_response(filepath)
-APP_VER = "985" # muss mit APP_VER in app.js übereinstimmen
+APP_VER = "986" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
diff --git a/backend/routes/contact.py b/backend/routes/contact.py
new file mode 100644
index 0000000..ee33cf4
--- /dev/null
+++ b/backend/routes/contact.py
@@ -0,0 +1,52 @@
+"""
+BAN YARO — Öffentliches Kontaktformular (kein Login erforderlich)
+Für Impressum-Kontaktpflicht nach § 5 DDG.
+"""
+
+from fastapi import APIRouter, Request
+from pydantic import BaseModel, EmailStr, Field
+from typing import Annotated
+
+from mailer import send_email, email_html
+from ratelimit import check as rl_check
+
+router = APIRouter()
+
+CONTACT_MAIL = "hallo@banyaro.app"
+
+
+class ContactIn(BaseModel):
+ name: Annotated[str, Field(min_length=2, max_length=100)]
+ email: EmailStr
+ subject: Annotated[str, Field(min_length=3, max_length=150)]
+ message: Annotated[str, Field(min_length=10, max_length=3000)]
+
+
+@router.post("")
+async def submit_contact(payload: ContactIn, request: Request):
+ rl_check(request, max_requests=3, window_seconds=3600, key=f"contact_{payload.email}")
+
+ body = f"""
+
Neue Kontaktanfrage über das Impressum-Formular:
+
+ | Name |
+ {payload.name} |
+ | E-Mail |
+ {payload.email} |
+ | Betreff |
+ {payload.subject} |
+
+
+{payload.message}
+
"""
+
+ plain = f"Kontakt von {payload.name} ({payload.email})\nBetreff: {payload.subject}\n\n{payload.message}"
+
+ await send_email(
+ CONTACT_MAIL,
+ f"Kontakt: {payload.subject} — {payload.name}",
+ email_html(body),
+ plain,
+ )
+ return {"ok": True}
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 7f2f4f3..ad4311a 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '985'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '986'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
diff --git a/backend/static/js/pages/impressum.js b/backend/static/js/pages/impressum.js
index baacd1a..e05776d 100644
--- a/backend/static/js/pages/impressum.js
+++ b/backend/static/js/pages/impressum.js
@@ -24,11 +24,58 @@ window.Page_impressum = (() => {
Kontakt
-
+
E-Mail: hallo@banyaro.app
- Wir antworten in der Regel innerhalb von 24 Stunden.
+ Oder nutze das Formular — wir antworten in der Regel innerhalb von 24 Stunden.
+
+
@@ -72,7 +119,58 @@ window.Page_impressum = (() => {
`;
}
+ function _initContactForm(container) {
+ const form = container.querySelector('#contact-form');
+ const statusEl = container.querySelector('#cf-status');
+ const submitBtn = container.querySelector('#cf-submit');
+ if (!form) return;
+
+ form.addEventListener('submit', async e => {
+ e.preventDefault();
+ const name = container.querySelector('#cf-name').value.trim();
+ const email = container.querySelector('#cf-email').value.trim();
+ const subject = container.querySelector('#cf-subject').value.trim();
+ const message = container.querySelector('#cf-message').value.trim();
+
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Wird gesendet…';
+ statusEl.style.display = 'none';
+
+ try {
+ const res = await fetch('/api/contact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, email, subject, message }),
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}));
+ throw new Error(err.detail || 'Fehler beim Senden.');
+ }
+ statusEl.style.display = 'block';
+ statusEl.style.background = 'var(--c-success-bg, #f0fdf4)';
+ statusEl.style.color = 'var(--c-success, #16a34a)';
+ statusEl.textContent = '✓ Nachricht gesendet — wir melden uns bald!';
+ form.reset();
+ } catch (err) {
+ statusEl.style.display = 'block';
+ statusEl.style.background = '#fef2f2';
+ statusEl.style.color = '#dc2626';
+ statusEl.textContent = err.message || 'Fehler beim Senden. Bitte versuche es später erneut.';
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Nachricht senden';
+ }
+ });
+ }
+
+ const _origInit = init;
+
function refresh() {}
- return { init, refresh };
+ return {
+ init(container) {
+ _origInit(container);
+ _initContactForm(container);
+ },
+ refresh
+ };
})();
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 27d268b..2f3405b 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
-const CACHE_VERSION = 'by-v985';
+const CACHE_VERSION = 'by-v986';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache