From 4738732517816b65d9b428573dbcb5bf1a1d0342 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Wed, 19 Nov 2025 13:22:30 +0100 Subject: [PATCH] feat: add landing page, enable mail-to-terminal, show success message closes #11 --- app/main.py | 92 +++--- app/static/styles.css | 308 +++++++++++++++++- app/templates/index.html | 174 ++++++++++ .../{form.html => semesterapparat_form.html} | 7 +- 4 files changed, 531 insertions(+), 50 deletions(-) create mode 100644 app/templates/index.html rename app/templates/{form.html => semesterapparat_form.html} (94%) 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:

+
+ +
+ +
+ +
+

Semesterapparat

+

Antrag für die Einrichtung eines Semesterapparats

+
+ +
+
+ +
+
+ +
+

ELSA

+

Elektronischer Semesterapparat

+
+ Im Aufbau +
+
+
+ +
+
+

Hinweise

+

+ Weitere Informationen zu den Semesterapparaten und elektronischen Angeboten finden Sie auf den Seiten der Hochschulbibliothek. +

+ +
+
+
+ + + + + + + + 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

@@ -113,6 +113,11 @@ +
+ + +
+