diff --git a/app/main.py b/app/main.py
index f339833..5d7a1e9 100755
--- a/app/main.py
+++ b/app/main.py
@@ -2,16 +2,13 @@ import os
import re
import smtplib
from email.mime.text import MIMEText
-from typing import Optional
from xml.etree.ElementTree import Element, SubElement, tostring
-from dotenv import load_dotenv
from fastapi import FastAPI, Form, HTTPException, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
-load_dotenv()
app = FastAPI()
templates = Jinja2Templates(directory="app/templates")
@@ -22,22 +19,23 @@ app.mount("/static", StaticFiles(directory="app/static"), name="static")
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
# SMTP / email configuration via environment
+MAIL_ENABLED = os.getenv("MAIL_ENABLED", "false").lower() == "true"
SMTP_HOST = os.getenv("SMTP_HOST", "smtp")
SMTP_PORT = int(os.getenv("SMTP_PORT", "25"))
-SENDER_MAIL = os.getenv("SENDER_MAIL", "noreply@example.com")
+MAIL_FROM = os.getenv("MAIL_FROM", "noreply@example.com")
MAIL_TO = os.getenv("MAIL_TO", "destination@example.com")
-MAIL_USERNAME = os.getenv("MAIL_USERNAME")
-MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
-
-print(
- f"Using SMTP server {SMTP_HOST}:{SMTP_PORT} with user {MAIL_USERNAME}"
- f" to send from {SENDER_MAIL} to {MAIL_TO}"
-)
@app.get("/", response_class=HTMLResponse)
-async def show_form(request: Request):
- return templates.TemplateResponse("form.html", {"request": request})
+async def landing_page(request: Request):
+ """Landing page with service selection"""
+ return templates.TemplateResponse("index.html", {"request": request})
+
+
+@app.get("/semesterapparat", response_class=HTMLResponse)
+async def semesterapparat_form(request: Request):
+ """Semesterapparat form page"""
+ return templates.TemplateResponse("semesterapparat_form.html", {"request": request})
@app.post("/submit")
@@ -45,67 +43,67 @@ async def handle_form(
request: Request,
name: str = Form(...),
lastname: str = Form(...),
- title: Optional[str] = Form(None),
+ title: str = Form(...),
telno: str = Form(...),
mail: str = Form(...),
apparatsname: str = Form(...),
subject: str = Form(...),
semester_type: str = Form(...), # "summer" or "winter"
semester_year: str = Form(...),
- dauerapparat: bool = Form(False),
authorname: list[str] = Form(...),
year: list[str] = Form(...),
booktitle: list[str] = Form(...),
- signature: Optional[list[str]] = Form(None),
+ signature: list[str] = Form(...),
+ message: str = Form(default=""),
):
- # Build XML
- root = Element("form_submission")
-
- static_data = SubElement(root, "static")
- SubElement(static_data, "name").text = name
- SubElement(static_data, "lastname").text = lastname
- # Title is optional
- SubElement(static_data, "title").text = title or ""
- SubElement(static_data, "telno").text = telno
- SubElement(static_data, "mail").text = mail
- SubElement(static_data, "apparatsname").text = apparatsname
- SubElement(static_data, "subject").text = subject
- SubElement(static_data, "semester").text = f"{semester_type} {semester_year}"
- SubElement(static_data, "dauerapparat").text = "true" if dauerapparat else "false"
- # inside handle_form(), right after parameters are received, before building XML:
# Basic email validation (server-side)
if not EMAIL_REGEX.match(mail):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid email address format.",
)
+
+ # Build XML
+ root = Element("form_submission")
+
+ static_data = SubElement(root, "static")
+ SubElement(static_data, "name").text = name
+ SubElement(static_data, "lastname").text = lastname
+ SubElement(static_data, "title").text = title
+ SubElement(static_data, "telno").text = telno
+ SubElement(static_data, "mail").text = mail
+ SubElement(static_data, "apparatsname").text = apparatsname
+ SubElement(static_data, "subject").text = subject
+ SubElement(static_data, "semester").text = f"{semester_type} {semester_year}"
+ if message:
+ SubElement(static_data, "message").text = message
books = SubElement(root, "books")
for i in range(len(authorname)):
book = SubElement(books, "book")
SubElement(book, "authorname").text = authorname[i]
SubElement(book, "year").text = year[i]
SubElement(book, "title").text = booktitle[i]
- # Signature is optional; write empty string if not provided
- sig_value = ""
- if signature and i < len(signature):
- sig_raw = signature[i] or ""
- sig_value = sig_raw.strip()
- SubElement(book, "signature").text = sig_value
+ SubElement(book, "signature").text = signature[i]
xml_data = tostring(root, encoding="unicode")
# Send mail
msg = MIMEText(xml_data, "xml")
- msg["Subject"] = "Antrag für neuen Semesterapparat"
- msg["From"] = SENDER_MAIL
+ msg["Subject"] = "New Form Submission"
+ msg["From"] = MAIL_FROM
msg["To"] = MAIL_TO
- with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
- server.connect(SMTP_HOST, SMTP_PORT)
- # Login only if credentials are provided
- if MAIL_USERNAME and MAIL_PASSWORD:
- server.login(MAIL_USERNAME, MAIL_PASSWORD)
+ if MAIL_ENABLED:
+ with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
+ server.send_message(msg)
+ else:
+ print("=" * 80)
+ print("MAIL SENDING DISABLED - Would have sent:")
+ print(f"From: {MAIL_FROM}")
+ print(f"To: {MAIL_TO}")
+ print(f"Subject: {msg['Subject']}")
+ print("-" * 80)
+ print(xml_data)
+ print("=" * 80)
- server.send_message(msg, from_addr=SENDER_MAIL, to_addrs=[MAIL_TO])
-
- return RedirectResponse("/", status_code=303)
+ return RedirectResponse("/?success=true", status_code=303)
diff --git a/app/static/styles.css b/app/static/styles.css
index 7832fa7..60e5080 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -165,7 +165,8 @@ h2 {
input[type="text"],
input[type="email"],
input[type="number"],
-select {
+select,
+textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
@@ -174,10 +175,17 @@ select {
color: var(--text);
outline: none;
transition: border-color .2s ease, box-shadow .2s ease;
+ font-family: inherit;
+}
+
+textarea {
+ resize: vertical;
+ min-height: 80px;
}
input:focus,
-select:focus {
+select:focus,
+textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px var(--ring);
}
@@ -294,3 +302,299 @@ input[type="radio"] { accent-color: var(--control-accent); }
/* Darken secondary button and add spacing after tables */
.data-table + .btn { margin-top: 14px; }
+
+/* Landing page styles */
+.landing-hero {
+ text-align: center;
+ padding: 40px 0 20px;
+}
+
+.landing-hero h1 {
+ font-size: 2.5rem;
+ margin-bottom: 12px;
+}
+
+.hero-subtitle {
+ font-size: 1.15rem;
+ color: var(--muted);
+}
+
+.service-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 24px;
+ margin: 40px 0;
+}
+
+.service-card {
+ position: relative;
+ background: var(--card-bg);
+ border: 2px solid var(--border);
+ border-radius: 16px;
+ padding: 32px 24px;
+ text-decoration: none;
+ color: var(--text);
+ transition: all 0.3s ease;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.service-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0,0,0,.12);
+ border-color: var(--primary);
+}
+
+.service-card-disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+}
+
+.service-card-disabled:hover {
+ transform: none;
+ border-color: var(--border);
+}
+
+.service-icon {
+ width: 80px;
+ height: 80px;
+ background: var(--primary);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 20px;
+ font-size: 2.5rem;
+ color: #fff;
+}
+
+.service-card-disabled .service-icon {
+ background: var(--muted);
+}
+
+.service-card h2 {
+ margin: 0 0 12px;
+ font-size: 1.5rem;
+}
+
+.service-card p {
+ margin: 0;
+ color: var(--muted);
+ line-height: 1.6;
+}
+
+.service-arrow {
+ margin-top: auto;
+ padding-top: 20px;
+ font-size: 1.5rem;
+ color: var(--primary);
+}
+
+.service-card-disabled .service-arrow {
+ display: none;
+}
+
+.service-badge {
+ margin-top: 16px;
+ padding: 6px 12px;
+ background: #fef3c7;
+ color: #92400e;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+[data-theme="dark"] .service-badge {
+ background: #422006;
+ color: #fde68a;
+}
+
+.info-section {
+ margin: 40px 0;
+}
+
+.info-section h3 {
+ margin-top: 0;
+ margin-bottom: 16px;
+}
+
+.info-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.info-links a {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--primary-700);
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.info-links a:hover {
+ text-decoration: underline;
+}
+
+/* Modal styles */
+.modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+.modal-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,0.5);
+}
+
+.modal-content {
+ position: relative;
+ background: var(--card-bg);
+ border-radius: 16px;
+ max-width: 500px;
+ width: 100%;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
+}
+
+.modal-header {
+ padding: 24px 24px 16px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.warning-icon {
+ font-size: 2rem;
+ color: #f59e0b;
+}
+
+.modal-body {
+ padding: 24px;
+}
+
+.modal-body p {
+ margin: 0 0 12px;
+ line-height: 1.6;
+}
+
+.modal-body p:last-child {
+ margin-bottom: 0;
+}
+
+.modal-footer {
+ padding: 16px 24px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ justify-content: flex-end;
+}
+
+@media (max-width: 640px) {
+ .landing-hero h1 {
+ font-size: 2rem;
+ }
+
+ .service-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .info-links {
+ flex-direction: column;
+ }
+}
+
+/* Toast notification styles */
+.toast {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: var(--card-bg);
+ border: 1px solid #22c55e;
+ border-radius: 12px;
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
+ padding: 16px 20px;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ max-width: 400px;
+ z-index: 2000;
+ opacity: 0;
+ transform: translateX(450px);
+ transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.toast.show {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.toast-icon {
+ font-size: 1.5rem;
+ color: #22c55e;
+ flex-shrink: 0;
+}
+
+.toast-content {
+ flex: 1;
+}
+
+.toast-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+ color: var(--text);
+}
+
+.toast-message {
+ font-size: 0.9rem;
+ color: var(--muted);
+ line-height: 1.4;
+}
+
+.toast-close {
+ background: none;
+ border: none;
+ color: var(--muted);
+ cursor: pointer;
+ padding: 0;
+ font-size: 1.25rem;
+ line-height: 1;
+ flex-shrink: 0;
+ transition: color 0.2s ease;
+}
+
+.toast-close:hover {
+ color: var(--text);
+}
+
+@media (max-width: 640px) {
+ .toast {
+ top: 10px;
+ right: 10px;
+ left: 10px;
+ max-width: none;
+ transform: translateY(-100px);
+ }
+
+ .toast.show {
+ transform: translateY(0);
+ }
+}
diff --git a/app/templates/index.html b/app/templates/index.html
new file mode 100644
index 0000000..aed8ab4
--- /dev/null
+++ b/app/templates/index.html
@@ -0,0 +1,174 @@
+
+
+
+ PH Freiburg Bibliothek - Formulare
+
+
+
+
+
+
+
+
+
+ Willkommen
+ Bitte wählen Sie den gewünschten Service:
+
+
+
+
+
+
+
Hinweise
+
+ Weitere Informationen zu den Semesterapparaten und elektronischen Angeboten finden Sie auf den Seiten der Hochschulbibliothek.
+
+
+
+
+
+
+
+
+
+
+
+
Das ELSA-Formular befindet sich derzeit noch in der Entwicklung und steht noch nicht zur Verfügung.
+
Bitte versuchen Sie es zu einem späteren Zeitpunkt erneut.
+
+
+
+
+
+
+
+
+
+
+
Erfolgreich gesendet
+
Mail wurde geschickt, Sie erhalten demnächst mehr Informationen
+
+
+
+
+
+
+
diff --git a/app/templates/form.html b/app/templates/semesterapparat_form.html
similarity index 94%
rename from app/templates/form.html
rename to app/templates/semesterapparat_form.html
index 16cee9e..b9d548d 100755
--- a/app/templates/form.html
+++ b/app/templates/semesterapparat_form.html
@@ -25,7 +25,7 @@
- Static Information
+ Semesterapparatsinformationen