diff --git a/backend/auth.py b/backend/auth.py index 55c63fc..b2736f5 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -87,7 +87,7 @@ def get_current_user( user_id = int(payload["sub"]) with db() as conn: row = conn.execute( - "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified, luna_trial_until FROM users WHERE id=?", + "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified FROM users WHERE id=?", (user_id,) ).fetchone() @@ -131,10 +131,7 @@ def require_admin(user=Depends(get_current_user)): def require_social_media(user=Depends(get_current_user)): - """Dependency: Social-Media-Manager, Luna-Probezugang oder Admin.""" - from datetime import datetime as _dt - trial = user.get("luna_trial_until") - trial_active = bool(trial and _dt.utcnow().isoformat() < trial) - if not (user.get("is_social_media") or user["rolle"] == "admin" or trial_active): + """Dependency: Social-Media-Manager oder Admin.""" + if not (user.get("is_social_media") or user["rolle"] == "admin"): raise HTTPException(status.HTTP_403_FORBIDDEN, "Kein Zugriff.") return user diff --git a/backend/database.py b/backend/database.py index 8ef5362..5ea9f4a 100644 --- a/backend/database.py +++ b/backend/database.py @@ -701,28 +701,7 @@ def _migrate(conn_factory): CREATE INDEX IF NOT EXISTS idx_wiki_berichte_rasse ON wiki_berichte(rasse, created_at DESC); """) - # Hunde-Filme: Katalog + Bewertungen + Hund des Monats - conn.executescript(""" - CREATE TABLE IF NOT EXISTS movies ( - id TEXT PRIMARY KEY, - titel TEXT NOT NULL, - originaltitel TEXT, - jahr INTEGER, - genre TEXT, - typ TEXT NOT NULL DEFAULT 'film', - hund_rasse TEXT, - stirbt_der_hund INTEGER NOT NULL DEFAULT 0, - beschreibung TEXT, - bild_emoji TEXT DEFAULT '🐾', - imdb_rating REAL, - streaming TEXT, - sort_order INTEGER DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_movies_typ ON movies(typ); - CREATE INDEX IF NOT EXISTS idx_movies_jahr ON movies(jahr DESC); - """) - + # Hunde-Filme: Bewertungen + Hund des Monats conn.executescript(""" CREATE TABLE IF NOT EXISTS movie_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1582,35 +1561,6 @@ def _migrate(conn_factory): if 'from_account' not in existing_ol: conn.execute("ALTER TABLE outreach_log ADD COLUMN from_account TEXT DEFAULT 'partner'") - # Job-Bewerbungen + Luna-Probezugang - conn.executescript(""" - CREATE TABLE IF NOT EXISTS job_applications ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, - name TEXT NOT NULL, - email TEXT NOT NULL, - dog_name TEXT, - dog_rasse TEXT, - social_handle TEXT, - motivation TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - admin_note TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - reviewed_at TEXT - ); - CREATE INDEX IF NOT EXISTS idx_job_apps_status ON job_applications(status, created_at DESC); - CREATE TABLE IF NOT EXISTS job_application_docs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - application_id INTEGER NOT NULL REFERENCES job_applications(id) ON DELETE CASCADE, - filename TEXT NOT NULL, - file_path TEXT NOT NULL, - uploaded_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - """) - existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] - if 'luna_trial_until' not in existing_u: - conn.execute("ALTER TABLE users ADD COLUMN luna_trial_until TEXT") - # js_exercise_id zu training_exercises — verbindet training_exercises mit exercise_progress existing_te = [row[1] for row in conn.execute("PRAGMA table_info(training_exercises)").fetchall()] if 'js_exercise_id' not in existing_te: diff --git a/backend/main.py b/backend/main.py index d85556a..83fa934 100644 --- a/backend/main.py +++ b/backend/main.py @@ -46,8 +46,6 @@ logger = logging.getLogger(__name__) async def lifespan(app: FastAPI): logger.info("Ban Yaro startet...") init_db() - from routes.movies import seed_movies - seed_movies() logger.info(f"KI-Modus: {ki.KI_MODE}") sched.start() yield @@ -188,7 +186,6 @@ from routes.breeder_export import router as breeder_export_router from routes.zucht_ki import router as zucht_ki_router from routes.partner import router as partner_router from routes.outreach import router as outreach_router -from routes.jobs import router as jobs_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -222,7 +219,6 @@ app.include_router(breeder_export_router, prefix="/api", tags=["Export"]) app.include_router(zucht_ki_router, prefix="/api", tags=["Züchter-KI"]) app.include_router(partner_router, prefix="/api", tags=["Partner"]) app.include_router(outreach_router, prefix="/api/outreach", tags=["Outreach"]) -app.include_router(jobs_router, prefix="/api/jobs", tags=["Jobs"]) app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"]) app.include_router(profile_router, prefix="/api/profile", tags=["Profil"]) app.include_router(import_router, prefix="/api/import", tags=["Import"]) diff --git a/backend/routes/jobs.py b/backend/routes/jobs.py deleted file mode 100644 index 8714ae2..0000000 --- a/backend/routes/jobs.py +++ /dev/null @@ -1,318 +0,0 @@ -"""BAN YARO — Social-Media-Job Bewerbungs-System""" - -import os -import uuid -from datetime import datetime, timedelta -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form -from fastapi.responses import FileResponse -from typing import Optional -from database import db -from auth import get_current_user, get_current_user_optional, require_admin -from mailer import send_email, email_html - -router = APIRouter() - -MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") -JOBS_DIR = os.path.join(MEDIA_DIR, "jobs") -TRIAL_DAYS = 14 -MAX_FILES = 3 -MAX_FILE_MB = 10 - -os.makedirs(JOBS_DIR, exist_ok=True) - -_ALLOWED_EXT = {".pdf", ".jpg", ".jpeg", ".png", ".webp", ".mp4", ".mov"} - - -# ------------------------------------------------------------------ -# POST /api/jobs/apply -# ------------------------------------------------------------------ -async def apply( - name: str = Form(...), - email: str = Form(...), - dog_name: str = Form(""), - dog_rasse: str = Form(""), - social_handle: str = Form(...), - motivation: str = Form(...), - files: list[UploadFile] = File(default=[]), - user = Depends(get_current_user_optional), -): - if len(motivation.strip()) < 80: - raise HTTPException(400, "Bitte schreibe etwas mehr über dich (mindestens 80 Zeichen).") - if len(files) > MAX_FILES: - raise HTTPException(400, f"Maximal {MAX_FILES} Dateien erlaubt.") - - user_id = user["id"] if user else None - - # Doppelbewerbung verhindern - if user_id: - with db() as conn: - existing = conn.execute( - "SELECT id FROM job_applications WHERE user_id=? AND status NOT IN ('rejected')", - (user_id,) - ).fetchone() - if existing: - raise HTTPException(400, "Du hast bereits eine aktive Bewerbung eingereicht.") - - with db() as conn: - cur = conn.execute(""" - INSERT INTO job_applications - (user_id, name, email, dog_name, dog_rasse, social_handle, motivation) - VALUES (?,?,?,?,?,?,?) - """, (user_id, name.strip(), email.strip(), dog_name.strip(), - dog_rasse.strip(), social_handle.strip(), motivation.strip())) - app_id = cur.lastrowid - - # Dokumente speichern - app_dir = os.path.join(JOBS_DIR, str(app_id)) - os.makedirs(app_dir, exist_ok=True) - - for f in files: - if not f.filename: - continue - ext = os.path.splitext(f.filename)[1].lower() - if ext not in _ALLOWED_EXT: - continue - size = 0 - safe_name = f"{uuid.uuid4().hex}{ext}" - dest = os.path.join(app_dir, safe_name) - with open(dest, "wb") as out: - while chunk := await f.read(65536): - size += len(chunk) - if size > MAX_FILE_MB * 1024 * 1024: - out.close() - os.remove(dest) - raise HTTPException(400, f"Datei zu groß (max. {MAX_FILE_MB} MB).") - out.write(chunk) - conn.execute(""" - INSERT INTO job_application_docs (application_id, filename, file_path) - VALUES (?,?,?) - """, (app_id, f.filename, dest)) - - # Luna-Probezugang: 14 Tage ab sofort - if user_id: - trial_until = (datetime.utcnow() + timedelta(days=TRIAL_DAYS)).isoformat() - conn.execute( - "UPDATE users SET luna_trial_until=? WHERE id=?", - (trial_until, user_id) - ) - - # Bestätigungs-Mail an Bewerber - try: - body = f""" -

Hallo {name},

-

- deine Bewerbung als Social-Media-Manager/in bei Ban Yaro ist bei uns eingegangen. - Wir melden uns bald bei dir! -

- {"

🎉 Luna-Probezugang aktiviert!
Du hast für 14 Tage kostenlos Zugang zu Luna, unserem KI-Social-Media-Assistenten. Logge dich ein und probiere ihn aus.

" if user_id else ""} -

Das Ban Yaro Team

""" - await send_email( - email, - "Deine Bewerbung bei Ban Yaro 🐾", - email_html(body, cta_url="https://banyaro.app", cta_label="Zur App"), - f"Hallo {name}, deine Bewerbung ist eingegangen!", - ) - except Exception: - pass - - # Admin benachrichtigen - try: - admin_email = os.getenv("ADMIN_EMAIL", "") - if admin_email: - admin_body = f""" -

Neue Job-Bewerbung eingegangen:

- - - - - - -
Name{name}
E-Mail{email}
Hund{dog_name} ({dog_rasse})
Social{social_handle}
Anhänge{len([f for f in files if f.filename])} Datei(en)
-

{motivation[:300]}{"…" if len(motivation)>300 else ""}

""" - await send_email( - admin_email, - f"[Banyaro Jobs] Neue Bewerbung — {name}", - email_html(admin_body, cta_url="https://banyaro.app/#admin", cta_label="Im Admin-Bereich prüfen"), - f"Neue Bewerbung von {name} ({email})", - ) - except Exception: - pass - - return { - "ok": True, - "application_id": app_id, - "luna_trial": user_id is not None, - "trial_days": TRIAL_DAYS, - } - - -# FastAPI braucht expliziten Router-Decorator -router.add_api_route("/apply", apply, methods=["POST"], status_code=201) - - -# ------------------------------------------------------------------ -# GET /api/jobs/my-application -# ------------------------------------------------------------------ -@router.get("/my-application") -async def my_application(user=Depends(get_current_user)): - with db() as conn: - row = conn.execute( - """SELECT id, status, admin_note, created_at - FROM job_applications WHERE user_id=? - ORDER BY created_at DESC LIMIT 1""", - (user["id"],) - ).fetchone() - if not row: - return {"application": None} - return {"application": dict(row)} - - -# ------------------------------------------------------------------ -# GET /api/jobs/luna-trial-status -# ------------------------------------------------------------------ -@router.get("/luna-trial-status") -async def luna_trial_status(user=Depends(get_current_user)): - from datetime import datetime as _dt - trial = user.get("luna_trial_until") - if not trial: - return {"active": False} - remaining = (_dt.fromisoformat(trial) - _dt.utcnow()).days - return {"active": remaining > 0, "until": trial, "remaining_days": max(0, remaining)} - - -# ------------------------------------------------------------------ -# Admin: Bewerbungen verwalten -# ------------------------------------------------------------------ -@router.get("/admin/applications") -async def list_applications( - status: str = "pending", - admin = Depends(require_admin), -): - where = "" if status == "alle" else "WHERE a.status=?" - params = [] if status == "alle" else [status] - with db() as conn: - rows = conn.execute(f""" - SELECT a.*, u.name AS username, - COUNT(d.id) AS doc_count - FROM job_applications a - LEFT JOIN users u ON u.id = a.user_id - LEFT JOIN job_application_docs d ON d.application_id = a.id - {where} - GROUP BY a.id - ORDER BY a.created_at DESC - """, params).fetchall() - return [dict(r) for r in rows] - - -@router.get("/admin/applications/{app_id}") -async def get_application(app_id: int, admin=Depends(require_admin)): - with db() as conn: - row = conn.execute( - """SELECT a.*, u.name AS username, u.email AS user_email - FROM job_applications a - LEFT JOIN users u ON u.id = a.user_id - WHERE a.id=?""", - (app_id,) - ).fetchone() - if not row: - raise HTTPException(404) - docs = conn.execute( - "SELECT id, filename, uploaded_at FROM job_application_docs WHERE application_id=?", - (app_id,) - ).fetchall() - return {**dict(row), "docs": [dict(d) for d in docs]} - - -@router.patch("/admin/applications/{app_id}") -async def update_application( - app_id: int, - status: Optional[str] = None, - admin_note: Optional[str] = None, - admin = Depends(require_admin), -): - valid = {"pending", "reviewing", "accepted", "rejected"} - if status and status not in valid: - raise HTTPException(400, f"Ungültiger Status. Erlaubt: {valid}") - - with db() as conn: - row = conn.execute( - "SELECT user_id, email, name, status FROM job_applications WHERE id=?", - (app_id,) - ).fetchone() - if not row: - raise HTTPException(404) - - updates: dict = {"reviewed_at": datetime.utcnow().isoformat()} - if status: - updates["status"] = status - if admin_note is not None: - updates["admin_note"] = admin_note - - set_clause = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE job_applications SET {set_clause} WHERE id=?", - (*updates.values(), app_id) - ) - - # Bei Annahme: is_social_media aktivieren + Gründer-Status - if status == "accepted" and row["user_id"]: - conn.execute( - "UPDATE users SET is_social_media=1 WHERE id=?", - (row["user_id"],) - ) - founder_count = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if founder_count < 100: - conn.execute( - "UPDATE users SET is_founder=1 WHERE id=? AND is_founder=0", - (row["user_id"],) - ) - - # Status-Mail an Bewerber - try: - if status in ("accepted", "rejected", "reviewing"): - _send_status_mail(row["email"], row["name"], status, admin_note or "") - except Exception: - pass - - return {"ok": True} - - -@router.get("/admin/applications/{app_id}/docs/{doc_id}") -async def download_doc(app_id: int, doc_id: int, admin=Depends(require_admin)): - with db() as conn: - doc = conn.execute( - "SELECT file_path, filename FROM job_application_docs WHERE id=? AND application_id=?", - (doc_id, app_id) - ).fetchone() - if not doc or not os.path.exists(doc["file_path"]): - raise HTTPException(404) - return FileResponse(doc["file_path"], filename=doc["filename"]) - - -def _send_status_mail(email: str, name: str, status: str, note: str): - import asyncio - texts = { - "reviewing": ("Wir schauen uns deine Bewerbung genauer an 🐾", - f"

Hallo {name},

wir schauen uns deine Bewerbung gerade genauer an. Wir melden uns bald!

"), - "accepted": ("Herzlichen Glückwunsch — du bist dabei! 🎉", - f"

Hallo {name},

wir freuen uns, dir mitzuteilen: du bist unser neuer Social-Media-Manager/in für Ban Yaro!
Du erhältst außerdem den Gründer-Status in unserer Community. Willkommen im Team!

"), - "rejected": ("Deine Bewerbung bei Ban Yaro", - f"

Hallo {name},

vielen Dank für deine Bewerbung. Leider hat es diesmal nicht geklappt — aber wir wünschen dir alles Gute!

"), - } - subj, body_start = texts.get(status, ("Update zu deiner Bewerbung", f"

Hallo {name},

")) - note_html = f'
{note}
' if note else "" - body = body_start + note_html - - async def _send(): - await send_email(email, subj, email_html(body, cta_url="https://banyaro.app", cta_label="Zur App"), subj) - - try: - loop = asyncio.get_event_loop() - if loop.is_running(): - asyncio.ensure_future(_send()) - else: - loop.run_until_complete(_send()) - except Exception: - pass diff --git a/backend/routes/movies.py b/backend/routes/movies.py index 399c583..5ef83da 100644 --- a/backend/routes/movies.py +++ b/backend/routes/movies.py @@ -1,317 +1,140 @@ """BAN YARO — Hunde-Filme Routes""" -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Optional from datetime import datetime from database import db -from auth import get_current_user, get_current_user_optional, require_admin +from auth import get_current_user, get_current_user_optional router = APIRouter() # ------------------------------------------------------------------ -# Seed-Daten — werden beim ersten Start in die DB geschrieben +# Hardcoded Film-Daten # ------------------------------------------------------------------ -_SEED_FILME = [ - # ── Originalbestand ────────────────────────────────────────────── - {"id": "lassie", "titel": "Lassie", "jahr": 1943, "genre": "Familie", "typ": "film", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Der Klassiker schlechthin. Lassie findet immer nach Hause.", "bild_emoji": "🐕", "imdb_rating": 7.0}, - {"id": "benji", "titel": "Benji", "jahr": 1974, "genre": "Familie", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein herrenloser Hund rettet Kinder aus den Händen von Entführern.", "bild_emoji": "🐾", "imdb_rating": 6.4}, - {"id": "marley-and-me", "titel": "Marley & Ich", "jahr": 2008, "genre": "Drama/Komödie", "typ": "film", "hund_rasse": "Labrador", "stirbt_der_hund": True, "beschreibung": "Der chaotischste, aber liebste Labrador der Welt. Achtung: Taschentücher bereithalten.", "bild_emoji": "😭", "imdb_rating": 7.1}, - {"id": "hachiko", "titel": "Hachi: A Dog's Tale", "jahr": 2009, "genre": "Drama", "typ": "film", "hund_rasse": "Akita", "stirbt_der_hund": True, "beschreibung": "Basiert auf der wahren Geschichte des treuen Akita Hachikō. Starke emotionale Wirkung.", "bild_emoji": "💔", "imdb_rating": 8.1}, - {"id": "101-dalmatiner", "titel": "101 Dalmatiner", "jahr": 1961, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Dalmatiner", "stirbt_der_hund": False, "beschreibung": "Dalmatiner-Welpen vs. die böse Cruella de Vil. Animationsklassiker.", "bild_emoji": "🐡", "imdb_rating": 7.2}, - {"id": "beethoven", "titel": "Beethoven", "jahr": 1992, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Bernhardiner", "stirbt_der_hund": False, "beschreibung": "Riesiger Bernhardiner bringt Chaos ins Familienleben. Mehrere Fortsetzungen.", "bild_emoji": "🎵", "imdb_rating": 5.9}, - {"id": "rex", "titel": "Kommissar Rex", "jahr": 1994, "genre": "Krimi", "typ": "serie", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": False, "beschreibung": "Österreichische Krimiserie. Rex löst gemeinsam mit seinem Herrchen Verbrechen.", "bild_emoji": "🔍", "imdb_rating": 7.5}, - {"id": "old-yeller", "titel": "Old Yeller", "jahr": 1957, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": True, "beschreibung": "Amerikanischer Filmklassiker. Berühmtestes Filmende der Hundfilm-Geschichte.", "bild_emoji": "🌾", "imdb_rating": 7.3}, - {"id": "buddy", "titel": "Air Bud", "jahr": 1997, "genre": "Familie/Sport", "typ": "film", "hund_rasse": "Golden Retriever", "stirbt_der_hund": False, "beschreibung": "Hund spielt Basketball. Klingt absurd, wurde ein Hit.", "bild_emoji": "🏀", "imdb_rating": 5.7}, - {"id": "john-wick", "titel": "John Wick", "jahr": 2014, "genre": "Action", "typ": "film", "hund_rasse": "Beagle", "stirbt_der_hund": True, "beschreibung": "Achtung Spoiler: Der Hund stirbt am Anfang. Das löst die ganze Geschichte aus.", "bild_emoji": "💣", "imdb_rating": 7.4}, - {"id": "isle-of-dogs", "titel": "Isle of Dogs", "jahr": 2018, "genre": "Animation", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Wes Anderson Stopmotion-Meisterwerk. Alle Hunde Japans auf einer Insel verbannt.", "bild_emoji": "🏝️", "imdb_rating": 7.9}, - {"id": "eight-below", "titel": "8 Below", "jahr": 2006, "genre": "Abenteuer/Drama", "typ": "film", "hund_rasse": "Schlittenhunde", "stirbt_der_hund": True, "beschreibung": "Basiert auf wahren Ereignissen. Schlittenhunde überleben die Antarktis. Einige nicht.", "bild_emoji": "❄️", "imdb_rating": 7.3}, - # ── Animation / Kinder ────────────────────────────────────────── - {"id": "lady-and-the-tramp", "titel": "Susi und Strolch", "originaltitel": "Lady and the Tramp", "jahr": 1955, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Cocker Spaniel / Mischling", "stirbt_der_hund": False, "beschreibung": "Disney-Klassiker mit der berühmtesten Spaghetti-Szene der Filmgeschichte.", "bild_emoji": "🍝", "imdb_rating": 7.3, "streaming": "Disney+"}, - {"id": "fox-and-the-hound", "titel": "Cap und Capper", "originaltitel": "The Fox and the Hound", "jahr": 1981, "genre": "Animation/Familie", "typ": "film", "hund_rasse": "Bloodhound", "stirbt_der_hund": False, "beschreibung": "Emotionaler Disney-Film über Freundschaft zwischen Fuchs und Jagdhund — und wie die Welt sie trennt.", "bild_emoji": "🦊", "imdb_rating": 7.2, "streaming": "Disney+"}, - {"id": "balto", "titel": "Balto", "jahr": 1995, "genre": "Animation/Abenteuer", "typ": "film", "hund_rasse": "Husky/Wolf-Mischling", "stirbt_der_hund": False, "beschreibung": "1925 brachte Schlittenhund Balto lebensrettende Medizin nach Nome, Alaska. Basiert auf einer wahren Heldengeschichte.", "bild_emoji": "🐺", "imdb_rating": 7.1, "streaming": "Amazon Prime"}, - {"id": "bolt", "titel": "Bolt — Ein Hund für alle Fälle", "originaltitel": "Bolt", "jahr": 2008, "genre": "Animation/Abenteuer", "typ": "film", "hund_rasse": "Weißer Schäferhund", "stirbt_der_hund": False, "beschreibung": "Ein TV-Superhund glaubt, seine Kräfte seien echt, und reist abenteuerlich quer durch Amerika.", "bild_emoji": "⚡", "imdb_rating": 6.8, "streaming": "Disney+"}, - {"id": "frankenweenie", "titel": "Frankenweenie", "jahr": 2012, "genre": "Animation/Horrorkomödie","typ": "film","hund_rasse": "Bullterrier", "stirbt_der_hund": True, "beschreibung": "Tim Burtons Stop-Motion-Meisterwerk: Ein Junge erweckt seinen toten Hund mit Wissenschaft wieder zum Leben.", "bild_emoji": "🧟", "imdb_rating": 6.9, "streaming": "Disney+"}, - {"id": "secret-life-of-pets", "titel": "Pets — Geheimes Leben der Haustiere","originaltitel": "The Secret Life of Pets","jahr": 2016,"genre": "Animation/Komödie","typ": "film","hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Was machen unsere Haustiere, wenn wir nicht zu Hause sind? Rasante Antwort mit Witz und Charme.", "bild_emoji": "🏠", "imdb_rating": 6.5, "streaming": "Amazon Prime"}, - {"id": "plague-dogs", "titel": "The Plague Dogs", "jahr": 1982, "genre": "Animation/Drama", "typ": "film", "hund_rasse": "Labrador / Mischling", "stirbt_der_hund": True, "beschreibung": "Düsterer Animationsfilm für Erwachsene: Zwei Hunde fliehen aus einem Tierversuchs-Labor. Brutal ehrlich, nach Richard Adams.", "bild_emoji": "🚫", "imdb_rating": 7.7}, - {"id": "paw-patrol-movie", "titel": "PAW Patrol: Der Kinofilm", "originaltitel": "PAW Patrol: The Movie", "jahr": 2021, "genre": "Animation/Kinder", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Die beliebten TV-Rettungshunde auf der großen Leinwand. Für die jüngsten Fans ein Pflichtprogramm.", "bild_emoji": "🚒", "imdb_rating": 6.1, "streaming": "Amazon Prime"}, - # ── Klassiker vor 1980 ────────────────────────────────────────── - {"id": "the-thin-man", "titel": "Der dünne Mann", "originaltitel": "The Thin Man", "jahr": 1934, "genre": "Krimi/Komödie", "typ": "film", "hund_rasse": "Drahthaariger Foxterrier", "stirbt_der_hund": False, "beschreibung": "Hollywood-Klassiker mit Nick und Nora Charles — und Asta, dem witzigsten Hund der Filmgeschichte. Mehrere Fortsetzungen.", "bild_emoji": "🍸", "imdb_rating": 7.9}, - {"id": "lassie-come-home", "titel": "Lassie kehrt heim", "originaltitel": "Lassie Come Home", "jahr": 1943, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Der Originalfilm: Ein armes Farmkind muss seinen geliebten Collie verkaufen — Lassie findet trotzdem heim.", "bild_emoji": "🏡", "imdb_rating": 7.1}, - {"id": "incredible-journey", "titel": "Die unglaubliche Reise", "originaltitel": "The Incredible Journey", "jahr": 1963, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Bullterrier / Labrador", "stirbt_der_hund": False, "beschreibung": "Zwei Hunde und eine Katze meistern 400 km kanadische Wildnis — Disney-Abenteuer nach dem Roman von Sheila Burnford.", "bild_emoji": "🗺️", "imdb_rating": 7.0}, - {"id": "greyfriars-bobby", "titel": "Greyfriars Bobby", "jahr": 1961, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Skye Terrier", "stirbt_der_hund": False, "beschreibung": "Basiert auf der wahren Geschichte des Terriers, der 14 Jahre das Grab seines Herrchens in Edinburgh bewachte.", "bild_emoji": "⛪", "imdb_rating": 7.2, "streaming": "Disney+"}, - {"id": "sounder", "titel": "Sounder", "jahr": 1972, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Coonhound", "stirbt_der_hund": False, "beschreibung": "Oscar-nominiertes Drama über eine schwarze Farmfamilie in der Great Depression. Ihr Hund Sounder ist das Herz der Geschichte.", "bild_emoji": "🌾", "imdb_rating": 7.5}, - {"id": "where-red-fern-grows","titel": "Wo der rote Farn wächst", "originaltitel": "Where the Red Fern Grows", "jahr": 1974, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Coonhound", "stirbt_der_hund": True, "beschreibung": "Ein Junge spart jahrelang für zwei Jagdhunde. Kultfilm der amerikanischen Kindheit — das Ende lässt kaum jemanden trocken.", "bild_emoji": "🌿", "imdb_rating": 6.9}, - {"id": "milo-and-otis", "titel": "Milo und Otis", "originaltitel": "The Adventures of Milo and Otis","jahr": 1986,"genre": "Abenteuer/Familie","typ": "film", "hund_rasse": "Mops", "stirbt_der_hund": False, "beschreibung": "Japanischer Realfilm mit Katze und Hund auf großer Abenteuerreise. Für Kinder ein Klassiker, für Erwachsene nostalgisches Heimweh.", "bild_emoji": "🐾", "imdb_rating": 6.9}, - {"id": "umberto-d", "titel": "Umberto D.", "jahr": 1952, "genre": "Drama", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Meisterwerk des italienischen Neorealismus: Ein alter Rentner und sein Hund kämpfen würdevoll gegen Armut in Rom.", "bild_emoji": "🇮🇹", "imdb_rating": 8.1, "streaming": "Mubi"}, - # ── Wahre Geschichten ─────────────────────────────────────────── - {"id": "homeward-bound", "titel": "Auf dem Weg nach Hause", "originaltitel": "Homeward Bound", "jahr": 1993, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Bullterrier / Golden Retriever", "stirbt_der_hund": False, "beschreibung": "Zwei Hunde und eine Katze kämpfen sich mit Stimmen durch die amerikanische Wildnis nach Hause. Remake des Klassikers.", "bild_emoji": "🏔️", "imdb_rating": 7.0, "streaming": "Disney+"}, - {"id": "togo", "titel": "Togo", "jahr": 2019, "genre": "Abenteuer/Drama", "typ": "film", "hund_rasse": "Sibirischer Husky", "stirbt_der_hund": False, "beschreibung": "Die unbekannte Geschichte hinter dem Balto-Mythos: Der echte Held des Serum-Runs 1925 war Togo. Außergewöhnlicher Disney+-Film.", "bild_emoji": "🛷", "imdb_rating": 7.9, "streaming": "Disney+"}, - {"id": "red-dog", "titel": "Red Dog", "jahr": 2011, "genre": "Drama/Komödie", "typ": "film", "hund_rasse": "Kelpie", "stirbt_der_hund": True, "beschreibung": "Australischer Kultfilm über einen echten Wanderhund, der eine Minengemeinschaft im Outback zusammenbrachte. Rauh und herzlich.", "bild_emoji": "🦘", "imdb_rating": 7.3, "streaming": "Amazon Prime"}, - {"id": "megan-leavey", "titel": "Megan Leavey", "jahr": 2017, "genre": "Biopic/Drama", "typ": "film", "hund_rasse": "Belgischer Schäferhund (Malinois)", "stirbt_der_hund": False, "beschreibung": "Die wahre Geschichte einer US-Marine und ihres Sprengstoff-Suchhundes Rex im Irak-Einsatz. Kate Mara in einer ihrer stärksten Rollen.", "bild_emoji": "🎖️", "imdb_rating": 7.1, "streaming": "Amazon Prime"}, - {"id": "arthur-the-king", "titel": "Arthur der König", "originaltitel": "Arthur the King", "jahr": 2024, "genre": "Abenteuer/Drama", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Mark Wahlberg und ein streunender Hund meistern gemeinsam ein Extremrennen durch die Dominikanische Republik. Inspiriert von wahren Ereignissen.","bild_emoji": "🏆", "imdb_rating": 7.0, "streaming": "Amazon Prime"}, - {"id": "rescued-by-ruby", "titel": "Gerettet von Ruby", "originaltitel": "Rescued by Ruby", "jahr": 2022, "genre": "Biopic/Familie", "typ": "film", "hund_rasse": "Australian Shepherd / Border Collie","stirbt_der_hund": False,"beschreibung": "Ein Polizist und ein Tierheim-Hund retten sich gegenseitig — wahre Geschichte aus Rhode Island.", "bild_emoji": "🌟", "imdb_rating": 7.2, "streaming": "Netflix"}, - {"id": "my-dog-skip", "titel": "Mein Hund Skip", "originaltitel": "My Dog Skip", "jahr": 2000, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Jack Russell Terrier", "stirbt_der_hund": True, "beschreibung": "Die Coming-of-Age-Geschichte eines einsamen Jungen im Mississippi der 1940er, der durch seinen Hund Skip Freundschaft findet.", "bild_emoji": "📚", "imdb_rating": 7.0}, - # ── Arbeitshunde / Polizeihunde ───────────────────────────────── - {"id": "turner-and-hooch", "titel": "Turner & Hooch", "jahr": 1989, "genre": "Krimi/Komödie", "typ": "film", "hund_rasse": "Dogue de Bordeaux", "stirbt_der_hund": True, "beschreibung": "Tom Hanks als ordentlicher Detective trifft auf Hooch, den sabbernden Chaoshund. Buddy-Cop-Klassiker mit überraschend emotionalem Ende.", "bild_emoji": "🕵️", "imdb_rating": 6.2, "streaming": "Disney+"}, - {"id": "k9", "titel": "K-9", "jahr": 1989, "genre": "Krimi/Komödie", "typ": "film", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": False, "beschreibung": "Jim Belushi als Drogenfahnder bekommt zwangsweise den eigenwilligen Schäferhund Jerry als Partner. Klassiker des Buddy-Cop-Genres.", "bild_emoji": "🚔", "imdb_rating": 6.2}, - {"id": "max", "titel": "Max", "jahr": 2015, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Belgischer Schäferhund (Malinois)", "stirbt_der_hund": False, "beschreibung": "Ein Kriegshund aus Afghanistan wird nach dem Tod seines Handlers von dessen Familie adoptiert. Über Trauma und Vertrauen.", "bild_emoji": "🎗️", "imdb_rating": 6.6, "streaming": "Amazon Prime"}, - {"id": "dog-2022", "titel": "Dog", "jahr": 2022, "genre": "Drama/Komödie", "typ": "film", "hund_rasse": "Belgischer Schäferhund (Malinois)", "stirbt_der_hund": False, "beschreibung": "Channing Tatum fährt mit einem traumatisierten Kriegshund quer durch Amerika — roh, komisch und berührend.", "bild_emoji": "🚗", "imdb_rating": 6.5, "streaming": "Amazon Prime"}, - {"id": "quill", "titel": "Quill — Ein Führhund", "originaltitel": "Quill: The Life of a Guide Dog","jahr": 2004,"genre": "Drama/Familie", "typ": "film", "hund_rasse": "Labrador", "stirbt_der_hund": True, "beschreibung": "Japanischer Film über das Leben des Führhundes Quill vom Welpen bis zum Tod. Zeigt die unersetzliche Arbeit von Blindenführhunden.", "bild_emoji": "👁️", "imdb_rating": 7.1}, - # ── Komödien ──────────────────────────────────────────────────── - {"id": "beethoven-2", "titel": "Beethoven's 2nd", "jahr": 1993, "genre": "Familie/Komödie", "typ": "film", "hund_rasse": "Bernhardiner", "stirbt_der_hund": False, "beschreibung": "Beethoven verliebt sich und bekommt Nachwuchs — vier chaotische Welpen bringen die Familie erneut an den Rand des Nervenzusammenbruchs.", "bild_emoji": "🐶", "imdb_rating": 5.4}, - {"id": "dog-days-2018", "titel": "Dog Days", "jahr": 2018, "genre": "Komödie/Romanze", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Mehrere Angelenos und ihre Hunde, deren Leben sich charmant verflechten. Leichte Feel-Good-Komödie mit Vanessa Hudgens.", "bild_emoji": "☀️", "imdb_rating": 6.3}, - {"id": "as-good-as-it-gets", "titel": "Besser geht's nicht", "originaltitel": "As Good as It Gets", "jahr": 1997, "genre": "Komödie/Drama", "typ": "film", "hund_rasse": "Griffon Bruxellois", "stirbt_der_hund": False, "beschreibung": "Jack Nicholson als Misanthrop, der durch einen kleinen Hund namens Verdell sein Herz entdeckt. Oscar-Gewinner, zeitlos witzig.", "bild_emoji": "💊", "imdb_rating": 7.7, "streaming": "Amazon Prime"}, - {"id": "eat-pray-bark", "titel": "Eat Pray Bark", "jahr": 2026, "genre": "Komödie", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Deutsche Komödie über fünf exzentrische Hundebesitzer, die gemeinsam einen Hundetrainer in den Tiroler Bergen aufsuchen. Top 10 in 49 Netflix-Ländern.", "bild_emoji": "🏔️", "imdb_rating": None, "streaming": "Netflix"}, - {"id": "the-artist", "titel": "The Artist", "jahr": 2011, "genre": "Drama/Komödie", "typ": "film", "hund_rasse": "Jack Russell Terrier", "stirbt_der_hund": False, "beschreibung": "Oscar-Gewinner als Stummfilm-Hommage. Uggie der Jack Russell stahl allen die Show und gewann den Palm Dog Award in Cannes.", "bild_emoji": "🎬", "imdb_rating": 7.8, "streaming": "Amazon Prime"}, - # ── Thriller / Action / Horror ────────────────────────────────── - {"id": "cujo", "titel": "Cujo", "jahr": 1983, "genre": "Horror/Thriller", "typ": "film", "hund_rasse": "Bernhardiner", "stirbt_der_hund": True, "beschreibung": "Stephen Kings Roman verfilmt: Ein tollwütiger Bernhardiner terrorisiert eine Mutter und ihr Kind in einem Auto. Klassiker des 80er-Horror.", "bild_emoji": "🩸", "imdb_rating": 6.1, "streaming": "Amazon Prime"}, - {"id": "white-god", "titel": "White God — Hund ohne Gnade","originaltitel": "White God", "jahr": 2014, "genre": "Drama/Thriller", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": True, "beschreibung": "Ungarischer Arthouse-Thriller mit 250 echten Straßenhunden. Ein Mädchen sucht seinen Hund — während die Hunde Rache nehmen. Cannes-Preis.", "bild_emoji": "🔴", "imdb_rating": 6.8}, - {"id": "dogman-2018", "titel": "Dogman", "jahr": 2018, "genre": "Krimi/Drama", "typ": "film", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Matteo Garrones preisgekrönter Film: Ein stiller Hundegroomer verstrickt sich mit einem brutalen Kriminellen. Cannes-Gewinner 2018.", "bild_emoji": "✂️", "imdb_rating": 7.2, "streaming": "Amazon Prime"}, - {"id": "call-of-the-wild", "titel": "Ruf der Wildnis", "originaltitel": "The Call of the Wild", "jahr": 2020, "genre": "Abenteuer/Drama", "typ": "film", "hund_rasse": "Saint Bernard Mix (CGI)", "stirbt_der_hund": False, "beschreibung": "Jack Londons Klassiker mit Harrison Ford: Hund Buck wandert vom Salon-Leben in die Wildnis des Klondike. Episch.", "bild_emoji": "🌲", "imdb_rating": 6.7, "streaming": "Disney+"}, - # ── Deutsche / österreichische Produktionen ───────────────────── - {"id": "lassie-neues-abenteuer","titel": "Lassie — Ein neues Abenteuer","jahr": 2023, "genre": "Familie/Abenteuer","typ": "film", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Deutsche Neuinterpretation: Lassie hilft Kindern dabei, mysteriöse Hundesdiebstähle aufzudecken. Für Kinder und Familien.", "bild_emoji": "🐕", "imdb_rating": 5.6}, - # ── Neuere Serien ─────────────────────────────────────────────── - {"id": "turner-hooch-serie", "titel": "Turner & Hooch (Serie)", "originaltitel": "Turner & Hooch", "jahr": 2021, "genre": "Krimi/Komödie", "typ": "serie", "hund_rasse": "Dogue de Bordeaux", "stirbt_der_hund": False, "beschreibung": "Disney+-Sequel zur Filmklassik: Der Sohn des Originals detektiviert mit Hoochs Nachfolger. Nach einer Staffel abgesetzt.", "bild_emoji": "📺", "imdb_rating": 6.6, "streaming": "Disney+"}, - {"id": "healing-powers-of-dude","titel": "Dude, mein Hund", "originaltitel": "The Healing Powers of Dude", "jahr": 2020, "genre": "Komödie/Familie", "typ": "serie", "hund_rasse": "Labradoodle", "stirbt_der_hund": False, "beschreibung": "Netflix-Jugendserie: Ein Junge mit sozialer Angststörung und sein emotionaler Support-Hund meistern gemeinsam die Middle School.", "bild_emoji": "💙", "imdb_rating": 6.6, "streaming": "Netflix"}, - {"id": "hudson-rex", "titel": "Hudson & Rex", "jahr": 2019, "genre": "Krimi", "typ": "serie", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": False, "beschreibung": "Kanadische Neuauflage von Kommissar Rex: Detective Hudson und sein Schäferhund Rex lösen in Neufundland Verbrechen. Läuft seit 2019.", "bild_emoji": "🍁", "imdb_rating": 7.4}, - # ── Klassische Serien ─────────────────────────────────────────── - {"id": "lassie-serie", "titel": "Lassie (TV-Serie)", "originaltitel": "Lassie", "jahr": 1954, "genre": "Familie/Abenteuer", "typ": "serie", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Die legendäre CBS-Serie, die 20 Jahre lief und Generationen prägte. Lassies wöchentliche Rettungsaktionen wurden zum Inbegriff des Treue-Hundes.", "bild_emoji": "📡", "imdb_rating": 7.5}, - {"id": "rin-tin-tin-serie", "titel": "Rin Tin Tin", "originaltitel": "The Adventures of Rin Tin Tin","jahr": 1954, "genre": "Western/Familie", "typ": "serie", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": False, "beschreibung": "Legendäre 1950er-Western-Serie. Ein Waisenjunge und sein Schäferhund helfen der US-Kavallerie im Wilden Westen.", "bild_emoji": "🤠", "imdb_rating": 7.0}, - # ── Dokumentationen ───────────────────────────────────────────── - {"id": "dogs-netflix", "titel": "Dogs", "jahr": 2018, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Herzzerreißende Netflix-Dokuserie über die Bindung zwischen Hunden und Menschen weltweit. Sechs Episoden, Tränen garantiert.", "bild_emoji": "❤️", "imdb_rating": 8.0, "streaming": "Netflix"}, - {"id": "pick-of-the-litter", "titel": "Pick of the Litter", "jahr": 2018, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Labrador", "stirbt_der_hund": False, "beschreibung": "Ein Labrador-Wurf wird zur Führhundausbildung bestimmt. Nicht alle schaffen es — spannend wie ein Spielfilm.", "bild_emoji": "🎗️", "imdb_rating": 7.6, "streaming": "Amazon Prime"}, - {"id": "inside-mind-of-dog", "titel": "Im Kopf des Hundes", "originaltitel": "Inside the Mind of a Dog", "jahr": 2024, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Netflix-Wissenschaftsdoku: Was wissen Hunde wirklich über uns? Rob Lowe erzählt, Forscher erklären die Kognition unserer Vierbeiner.", "bild_emoji": "🧠", "imdb_rating": 7.2, "streaming": "Netflix"}, - {"id": "stray-doku", "titel": "Stray", "jahr": 2020, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Poetische Doku aus Hundeperspektive: Die Kamera folgt drei Straßenhunden durch Istanbul. Philosophisch, still und ungemein berührend.", "bild_emoji": "🕌", "imdb_rating": 6.9, "streaming": "Amazon Prime"}, - {"id": "dog-by-dog", "titel": "Dog by Dog", "jahr": 2015, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Erschütternde Doku über den milliardenschweren Welpen-Industrie-Komplex in den USA. Folgt dem Geldfluss hinter Puppy Mills.", "bild_emoji": "💰", "imdb_rating": 8.8, "streaming": "Netflix"}, - {"id": "gunthers-millions", "titel": "Günthers Millionen", "originaltitel": "Gunther's Millions", "jahr": 2023, "genre": "Dokumentation", "typ": "doku", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": False, "beschreibung": "Die absurde Netflix-Doku: Erbte ein Schäferhund wirklich eine halbe Milliarde Euro? Die Wahrheit ist noch seltsamer.", "bild_emoji": "💎", "imdb_rating": 5.6, "streaming": "Netflix"}, - # ── Weitere ───────────────────────────────────────────────────── - {"id": "a-dogs-purpose", "titel": "Bailey — Ein Freund fürs Leben","originaltitel": "A Dog's Purpose", "jahr": 2017, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Verschiedene (Labrador, Corgi u.a.)","stirbt_der_hund": True, "beschreibung": "Ein Hund wird mehrfach wiedergeboren und sucht in jeder Inkarnation nach seinem Sinn. Taschentücher-Pflicht.", "bild_emoji": "🔄", "imdb_rating": 7.3, "streaming": "Amazon Prime"}, - {"id": "a-dogs-journey", "titel": "Bailey 2", "originaltitel": "A Dog's Journey", "jahr": 2019, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Beagle / Bernhardiner u.a.", "stirbt_der_hund": True, "beschreibung": "Fortsetzung: Bailey beschützt in mehreren Leben die Enkelin seines Herrchens. Emotional und rührend.", "bild_emoji": "🔄", "imdb_rating": 7.5, "streaming": "Amazon Prime"}, - {"id": "art-of-racing", "titel": "Enzo und die wundersame Welt der Menschen","originaltitel": "The Art of Racing in the Rain","jahr": 2019,"genre": "Drama","typ": "film","hund_rasse": "Golden Retriever", "stirbt_der_hund": True, "beschreibung": "Ein Golden Retriever erzählt seine Lebensgeschichte. Philosophisch, witzig, herzzerreißend — Kevin Costner leiht ihm die Stimme.", "bild_emoji": "🏎️", "imdb_rating": 7.6, "streaming": "Disney+"}, - {"id": "dog-gone", "titel": "Dog Gone", "originaltitel": "Dog Gone", "jahr": 2023, "genre": "Drama/Familie", "typ": "film", "hund_rasse": "Golden Retriever", "stirbt_der_hund": False, "beschreibung": "Vater und Sohn suchen auf dem Appalachian Trail ihren verlorenen kranken Hund — und finden dabei zueinander. Wahre Geschichte.", "bild_emoji": "🥾", "imdb_rating": 6.1, "streaming": "Netflix"}, - {"id": "white-fang", "titel": "Wolfsblut", "originaltitel": "White Fang", "jahr": 1991, "genre": "Abenteuer/Familie", "typ": "film", "hund_rasse": "Wolf-Hund-Hybrid", "stirbt_der_hund": False, "beschreibung": "Jack Londons Klassiker mit Ethan Hawke: Ein Wolf-Hund-Hybrid findet in Alaska zwischen Wildnis und Menschenwelt seinen Platz.", "bild_emoji": "❄️", "imdb_rating": 6.7}, - {"id": "because-of-winn-dixie","titel": "Winn-Dixie", "originaltitel": "Because of Winn-Dixie", "jahr": 2005, "genre": "Familie/Drama", "typ": "film", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein einsames Mädchen findet einen streunenden Hund im Supermarkt — und mit ihm eine ganze Gemeinschaft.", "bild_emoji": "🛒", "imdb_rating": 6.4}, - {"id": "lassie-2005", "titel": "Lassie (2005)", "jahr": 2005, "genre": "Familie/Abenteuer", "typ": "film", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Britische Neuverfilmung mit Peter O'Toole. Lassie flieht aus Schottland und findet den langen Weg nach Yorkshire. Atmosphärisch.", "bild_emoji": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "imdb_rating": 6.7}, +FILME = [ + {"id": "lassie", "titel": "Lassie", "jahr": 1943, "genre": "Familie", "hund_rasse": "Collie", "stirbt_der_hund": False, "beschreibung": "Der Klassiker schlechthin. Lassie findet immer nach Hause.", "bild_emoji": "🐕", "bewertung_avg": 4.2}, + {"id": "benji", "titel": "Benji", "jahr": 1974, "genre": "Familie", "hund_rasse": "Mischling", "stirbt_der_hund": False, "beschreibung": "Ein herrenloser Hund rettet Kinder aus den Händen von Entführern.", "bild_emoji": "🐾", "bewertung_avg": 4.0}, + {"id": "marley-and-me", "titel": "Marley & Ich", "jahr": 2008, "genre": "Drama/Komödie", "hund_rasse": "Labrador", "stirbt_der_hund": True, "beschreibung": "Der chaotischste, aber liebste Labrador der Welt. Achtung: Taschentücher bereithalten.", "bild_emoji": "😭", "bewertung_avg": 4.5}, + {"id": "hachiko", "titel": "Hachi: A Dog's Tale", "jahr": 2009, "genre": "Drama", "hund_rasse": "Akita", "stirbt_der_hund": True, "beschreibung": "Basiert auf der wahren Geschichte des treuen Akita Hachikō. Starke emotionale Wirkung.", "bild_emoji": "💔", "bewertung_avg": 4.8}, + {"id": "101-dalmatiner", "titel": "101 Dalmatiner", "jahr": 1961, "genre": "Animation/Familie", "hund_rasse": "Dalmatiner", "stirbt_der_hund": False, "beschreibung": "Dalmatiner-Welpen vs. die böse Cruella de Vil. Animationsklassiker.", "bild_emoji": "🐡", "bewertung_avg": 4.3}, + {"id": "beethoven", "titel": "Beethoven", "jahr": 1992, "genre": "Familie/Komödie", "hund_rasse": "Bernhardiner", "stirbt_der_hund": False, "beschreibung": "Riesiger Bernhardiner bringt Chaos ins Familienleben. Mehrere Fortsetzungen.", "bild_emoji": "🎵", "bewertung_avg": 3.8}, + {"id": "rex", "titel": "Kommissar Rex", "jahr": 1994, "genre": "Krimi/Serie", "hund_rasse": "Deutscher Schäferhund", "stirbt_der_hund": False, "beschreibung": "Österreichische Krimiserie. Rex löst gemeinsam mit seinem Herrchen Verbrechen.", "bild_emoji": "🔍", "bewertung_avg": 4.1}, + {"id": "old-yeller", "titel": "Old Yeller", "jahr": 1957, "genre": "Familie/Drama", "hund_rasse": "Mischling", "stirbt_der_hund": True, "beschreibung": "Amerikanischer Filmklassiker. Berühmtestes Filmende der Hundfilm-Geschichte.", "bild_emoji": "🌾", "bewertung_avg": 4.0}, + {"id": "buddy", "titel": "Air Bud", "jahr": 1997, "genre": "Familie/Sport", "hund_rasse": "Golden Retriever", "stirbt_der_hund": False, "beschreibung": "Hund spielt Basketball. Klingt absurd, wurde ein Hit.", "bild_emoji": "🏀", "bewertung_avg": 3.5}, + {"id": "john-wick", "titel": "John Wick", "jahr": 2014, "genre": "Action", "hund_rasse": "Beagle", "stirbt_der_hund": True, "beschreibung": "Achtung Spoiler: Der Hund stirbt am Anfang. Das löst die ganze Geschichte aus. Kontroversiell beliebt.", "bild_emoji": "💣", "bewertung_avg": 4.6}, + {"id": "isle-of-dogs", "titel": "Isle of Dogs", "jahr": 2018, "genre": "Animation", "hund_rasse": "Verschiedene", "stirbt_der_hund": False, "beschreibung": "Wes Anderson Stopmotion-Meisterwerk. Alle Hunde Japans auf einer Insel verbannt.", "bild_emoji": "🏝️", "bewertung_avg": 4.4}, + {"id": "eight-below", "titel": "8 Below", "jahr": 2006, "genre": "Abenteuer/Drama", "hund_rasse": "Schlittenhunde", "stirbt_der_hund": True, "beschreibung": "Basiert auf wahren Ereignissen. Schlittenhunde überleben die Antarktis. Einige nicht.", "bild_emoji": "❄️", "bewertung_avg": 4.3}, ] -_SEED_PROMIS = [ - {"name": "Hachikō", "rasse": "Akita Inu", "bekannt_fuer": "9 Jahre täglich auf seinen verstorbenen Herrchen am Bahnhof Shibuya gewartet. Statue in Tokio.", "emoji": "🗿"}, - {"name": "Rin Tin Tin", "rasse": "Deutscher Schäferhund","bekannt_fuer": "Filmhund der 1920er. Rettete Warner Bros. vor dem Bankrott. Erster Hundestar Hollywoods.", "emoji": "🎬"}, - {"name": "Laika", "rasse": "Mischling", "bekannt_fuer": "Erstes Lebewesen im Weltall (Sputnik 2, 1957). Sowjetische Weltraumpionierin.", "emoji": "🚀"}, - {"name": "Endal", "rasse": "Labrador", "bekannt_fuer": "Assistenzhund in England. Erster Hund der eine EC-Karte am Geldautomaten benutzte.", "emoji": "💳"}, - {"name": "Barry", "rasse": "Bernhardiner", "bekannt_fuer": "Legendärer Rettungshund der Alpen (1800–1812). Soll 40 Menschen das Leben gerettet haben.", "emoji": "🏔️"}, - {"name": "Greyfriars Bobby", "rasse": "Skye Terrier", "bekannt_fuer": "14 Jahre das Grab seines Herrchens in Edinburgh bewacht. Statue und Pub benannt nach ihm.", "emoji": "⛪"}, - {"name": "Balto", "rasse": "Siberian Husky", "bekannt_fuer": "Führte 1925 den letzten Abschnitt des Serum-Runs nach Nome, Alaska. Statue im Central Park New York.", "emoji": "🛷"}, - {"name": "Togo", "rasse": "Siberian Husky", "bekannt_fuer": "Der echte Held des Serum-Runs 1925 — legte die schwierigste Strecke zurück, blieb aber lange unbekannt.", "emoji": "🏅"}, - {"name": "Asta", "rasse": "Drahthaariger Foxterrier","bekannt_fuer": "Filmhund in der 'Dünner Mann'-Reihe (1934–1947). Hollywood-Ikone der klassischen Ära.", "emoji": "🎩"}, - {"name": "Lassie", "rasse": "Rough Collie", "bekannt_fuer": "Meistverfilmter Hund der Geschichte. Erster Vierbeiner mit einem Stern auf dem Hollywood Walk of Fame.", "emoji": "⭐"}, +PROMIS = [ + {"name": "Hachikō", "rasse": "Akita Inu", "bekannt_fuer": "9 Jahre lang täglich auf seinen verstorbenen Herrchen am Bahnhof Shibuya gewartet. Statue in Tokio.", "emoji": "🗿"}, + {"name": "Rin Tin Tin", "rasse": "Deutscher Schäferhund", "bekannt_fuer": "Filmhund der 1920er-Jahre. Rettete Warner Bros. vor dem Bankrott. Erster Hundestar Hollywoods.", "emoji": "🎬"}, + {"name": "Laika", "rasse": "Mischling", "bekannt_fuer": "Erstes Lebewesen im Weltall (Sputnik 2, 1957). Wurde zur sowjetischen Weltraumpionierin.", "emoji": "🚀"}, + {"name": "Endal", "rasse": "Labrador", "bekannt_fuer": "Assistenzhund in England. Erster Hund der eine EC-Karte am Geldautomaten benutzte.", "emoji": "💳"}, + {"name": "Barry", "rasse": "Bernhardiner", "bekannt_fuer": "Legendärer Rettungshund der Alpen (1800–1812). Soll 40 Menschen das Leben gerettet haben.", "emoji": "🏔️"}, + {"name": "Greyfriars Bobby", "rasse": "Skye Terrier", "bekannt_fuer": "14 Jahre lang das Grab seines Herrchens in Edinburgh bewacht. Statue und Pub benannt nach ihm.", "emoji": "⛪"}, ] -def seed_movies(): - """Füllt die movies-Tabelle beim ersten Start (idempotent per INSERT OR IGNORE).""" - import logging - logger = logging.getLogger(__name__) - with db() as conn: - count = conn.execute("SELECT COUNT(*) FROM movies").fetchone()[0] - if count == 0: - for i, f in enumerate(_SEED_FILME): - conn.execute(""" - INSERT OR IGNORE INTO movies - (id, titel, originaltitel, jahr, genre, typ, hund_rasse, - stirbt_der_hund, beschreibung, bild_emoji, imdb_rating, - streaming, sort_order) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - """, ( - f["id"], f["titel"], f.get("originaltitel"), - f.get("jahr"), f.get("genre"), f.get("typ", "film"), - f.get("hund_rasse"), 1 if f.get("stirbt_der_hund") else 0, - f.get("beschreibung"), f.get("bild_emoji", "🐾"), - f.get("imdb_rating"), f.get("streaming"), i, - )) - logger.info(f"movies: {len(_SEED_FILME)} Filme geseedet.") - - # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class FilmVoteRequest(BaseModel): bewertung: int # 1–5 + class HundDesMonatsVoteRequest(BaseModel): dog_id: int -class MovieCreate(BaseModel): - id: str - titel: str - originaltitel: Optional[str] = None - jahr: Optional[int] = None - genre: Optional[str] = None - typ: str = "film" - hund_rasse: Optional[str] = None - stirbt_der_hund: bool = False - beschreibung: Optional[str] = None - bild_emoji: str = "🐾" - imdb_rating: Optional[float] = None - streaming: Optional[str] = None - -class MovieUpdate(BaseModel): - titel: Optional[str] = None - originaltitel: Optional[str] = None - jahr: Optional[int] = None - genre: Optional[str] = None - typ: Optional[str] = None - hund_rasse: Optional[str] = None - stirbt_der_hund: Optional[bool] = None - beschreibung: Optional[str] = None - bild_emoji: Optional[str] = None - imdb_rating: Optional[float] = None - streaming: Optional[str] = None - # ------------------------------------------------------------------ -# GET /api/movies/filme +# GET /api/movies/filme — Film-Liste mit optionaler User-Bewertung # ------------------------------------------------------------------ -_SORT_COLS = { - "titel": "m.titel ASC", - "jahr_desc": "m.jahr DESC", - "jahr_asc": "m.jahr ASC", - "imdb": "m.imdb_rating DESC", - "bewertung": "community_avg DESC", - "default": "m.sort_order ASC, m.jahr DESC", -} - @router.get("/filme") -async def get_filme( - sort: str = Query("default"), - typ: str = Query("alle"), # alle | film | serie | doku - user = Depends(get_current_user_optional), -): - order = _SORT_COLS.get(sort, _SORT_COLS["default"]) - - where = "" - params: list = [] - if typ != "alle": - where = "WHERE m.typ = ?" - params.append(typ) +async def get_filme(user=Depends(get_current_user_optional)): + user_ratings = {} + community_avgs = {} with db() as conn: - rows = conn.execute(f""" - SELECT m.*, - COALESCE(AVG(v.bewertung), 0) AS community_avg, - COUNT(v.id) AS bewertung_cnt, - uv.bewertung AS user_rating - FROM movies m - LEFT JOIN movie_votes v ON v.film_id = m.id - LEFT JOIN movie_votes uv ON uv.film_id = m.id - AND uv.user_id = ? - {where} - GROUP BY m.id - ORDER BY {order} - """, [user["id"] if user else None] + params).fetchall() + if user: + rows = conn.execute( + "SELECT film_id, bewertung FROM movie_votes WHERE user_id=?", + (user["id"],), + ).fetchall() + user_ratings = {r["film_id"]: r["bewertung"] for r in rows} + + avg_rows = conn.execute( + "SELECT film_id, AVG(bewertung) as avg_bew, COUNT(*) as cnt FROM movie_votes GROUP BY film_id" + ).fetchall() + community_avgs = {r["film_id"]: {"avg": round(r["avg_bew"], 1), "cnt": r["cnt"]} for r in avg_rows} result = [] - for r in rows: - d = dict(r) - d["stirbt_der_hund"] = bool(d["stirbt_der_hund"]) - d["bewertung_avg"] = round(d["community_avg"] or 0, 1) - result.append(d) + for film in FILME: + f = dict(film) + f["user_rating"] = user_ratings.get(film["id"]) + if film["id"] in community_avgs: + f["bewertung_avg"] = community_avgs[film["id"]]["avg"] + f["bewertung_cnt"] = community_avgs[film["id"]]["cnt"] + else: + f["bewertung_cnt"] = 0 + result.append(f) + return result # ------------------------------------------------------------------ -# POST /api/movies/filme/{film_id}/vote +# POST /api/movies/filme/{film_id}/vote — Bewertung abgeben (Upsert) # ------------------------------------------------------------------ @router.post("/filme/{film_id}/vote") async def vote_film(film_id: str, data: FilmVoteRequest, user=Depends(get_current_user)): + if not any(f["id"] == film_id for f in FILME): + raise HTTPException(404, "Film nicht gefunden.") if data.bewertung < 1 or data.bewertung > 5: raise HTTPException(400, "Bewertung muss zwischen 1 und 5 liegen.") + with db() as conn: - if not conn.execute("SELECT 1 FROM movies WHERE id=?", (film_id,)).fetchone(): - raise HTTPException(404, "Film nicht gefunden.") - conn.execute(""" - INSERT INTO movie_votes (user_id, film_id, bewertung) - VALUES (?, ?, ?) - ON CONFLICT(user_id, film_id) DO UPDATE SET bewertung=excluded.bewertung - """, (user["id"], film_id, data.bewertung)) + conn.execute( + """INSERT INTO movie_votes (user_id, film_id, bewertung) + VALUES (?, ?, ?) + ON CONFLICT(user_id, film_id) DO UPDATE SET bewertung=excluded.bewertung""", + (user["id"], film_id, data.bewertung), + ) row = conn.execute( "SELECT AVG(bewertung) as avg_bew, COUNT(*) as cnt FROM movie_votes WHERE film_id=?", (film_id,), ).fetchone() + return { - "film_id": film_id, + "film_id": film_id, "bewertung_avg": round(row["avg_bew"], 1) if row["avg_bew"] else data.bewertung, "bewertung_cnt": row["cnt"], - "user_rating": data.bewertung, + "user_rating": data.bewertung, } # ------------------------------------------------------------------ -# Admin: CRUD für Filme -# ------------------------------------------------------------------ -@router.post("/filme", status_code=201) -async def create_film(data: MovieCreate, admin=Depends(require_admin)): - with db() as conn: - max_order = conn.execute("SELECT COALESCE(MAX(sort_order),0) FROM movies").fetchone()[0] - try: - conn.execute(""" - INSERT INTO movies (id, titel, originaltitel, jahr, genre, typ, hund_rasse, - stirbt_der_hund, beschreibung, bild_emoji, imdb_rating, streaming, sort_order) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - """, (data.id, data.titel, data.originaltitel, data.jahr, data.genre, data.typ, - data.hund_rasse, 1 if data.stirbt_der_hund else 0, data.beschreibung, - data.bild_emoji, data.imdb_rating, data.streaming, max_order + 1)) - except Exception: - raise HTTPException(400, "Film-ID bereits vorhanden.") - return {"ok": True} - -@router.patch("/filme/{film_id}") -async def update_film(film_id: str, data: MovieUpdate, admin=Depends(require_admin)): - updates = {k: v for k, v in data.model_dump(exclude_none=True).items()} - if "stirbt_der_hund" in updates: - updates["stirbt_der_hund"] = 1 if updates["stirbt_der_hund"] else 0 - if not updates: - return {"ok": True} - set_clause = ", ".join(f"{k}=?" for k in updates) - with db() as conn: - conn.execute(f"UPDATE movies SET {set_clause} WHERE id=?", (*updates.values(), film_id)) - return {"ok": True} - -@router.delete("/filme/{film_id}") -async def delete_film(film_id: str, admin=Depends(require_admin)): - with db() as conn: - conn.execute("DELETE FROM movies WHERE id=?", (film_id,)) - return {"ok": True} - - -# ------------------------------------------------------------------ -# GET /api/movies/promis — Berühmte Hunde (aus Seed-Daten) -# ------------------------------------------------------------------ -@router.get("/promis") -async def get_promis(): - return _SEED_PROMIS - - -# ------------------------------------------------------------------ -# Hund des Monats +# GET /api/movies/hund-des-monats — Top-Votes des aktuellen Monats # ------------------------------------------------------------------ @router.get("/hund-des-monats") async def get_hund_des_monats(user=Depends(get_current_user_optional)): monat = datetime.now().strftime("%Y-%m") + with db() as conn: - rows = conn.execute(""" - SELECT d.id, d.name, d.rasse, d.foto_url, u.name as besitzer_name, - COUNT(v.id) as stimmen - FROM hund_des_monats_votes v - JOIN dogs d ON d.id = v.dog_id - JOIN users u ON u.id = d.user_id - WHERE v.monat = ? - GROUP BY v.dog_id - ORDER BY stimmen DESC - LIMIT 10 - """, (monat,)).fetchall() + rows = conn.execute( + """SELECT d.id, d.name, d.rasse, d.foto_url, u.name as besitzer_name, + COUNT(v.id) as stimmen + FROM hund_des_monats_votes v + JOIN dogs d ON d.id = v.dog_id + JOIN users u ON u.id = d.user_id + WHERE v.monat = ? + GROUP BY v.dog_id + ORDER BY stimmen DESC + LIMIT 10""", + (monat,), + ).fetchall() + user_vote = None if user: row = conn.execute( @@ -320,25 +143,43 @@ async def get_hund_des_monats(user=Depends(get_current_user_optional)): ).fetchone() if row: user_vote = row["dog_id"] - return {"monat": monat, "top": [dict(r) for r in rows], "user_vote": user_vote} + + return { + "monat": monat, + "top": [dict(r) for r in rows], + "user_vote": user_vote, + } +# ------------------------------------------------------------------ +# POST /api/movies/hund-des-monats/vote — Abstimmen (Auth required) +# ------------------------------------------------------------------ @router.post("/hund-des-monats/vote") async def vote_hund_des_monats(data: HundDesMonatsVoteRequest, user=Depends(get_current_user)): monat = datetime.now().strftime("%Y-%m") + with db() as conn: - dog = conn.execute("SELECT id, user_id, is_public FROM dogs WHERE id=?", (data.dog_id,)).fetchone() + # Prüfen ob Hund existiert und entweder dem User gehört oder öffentlich ist + dog = conn.execute( + "SELECT id, user_id, is_public FROM dogs WHERE id=?", + (data.dog_id,), + ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") if dog["user_id"] != user["id"] and not dog["is_public"]: raise HTTPException(403, "Dieser Hund ist nicht öffentlich.") - conn.execute(""" - INSERT INTO hund_des_monats_votes (user_id, dog_id, monat) - VALUES (?, ?, ?) - ON CONFLICT(user_id, monat) DO UPDATE SET dog_id=excluded.dog_id - """, (user["id"], data.dog_id, monat)) + + conn.execute( + """INSERT INTO hund_des_monats_votes (user_id, dog_id, monat) + VALUES (?, ?, ?) + ON CONFLICT(user_id, monat) DO UPDATE SET dog_id=excluded.dog_id""", + (user["id"], data.dog_id, monat), + ) + + # Aktuelle Stimmenanzahl für den gewählten Hund row = conn.execute( "SELECT COUNT(*) as cnt FROM hund_des_monats_votes WHERE dog_id=? AND monat=?", (data.dog_id, monat), ).fetchone() + return {"dog_id": data.dog_id, "monat": monat, "stimmen": row["cnt"]} diff --git a/backend/static/js/app.js b/backend/static/js/app.js index b75494f..cdc5231 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -70,7 +70,6 @@ const App = (() => { zuchthunde: { title: 'Zuchtkartei', module: null, requiresAuth: true }, 'zucht-profil': { title: 'Hunde-Profil', module: null }, gruender: { title: '100 Gründer', module: null }, - jobs: { title: 'Wir suchen dich', module: null }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 37d31ae..1775ccd 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -18,8 +18,7 @@ window.Page_admin = (() => { { id: 'social', label: 'Social Media', icon: 'camera' }, { id: 'analytics', label: 'Analytics', icon: 'target' }, { id: 'system', label: 'System', icon: 'gear' }, - { id: 'jobs', label: 'Scheduler', icon: 'clock' }, - { id: 'bewerbungen', label: 'Bewerbungen', icon: 'user-plus' }, + { id: 'jobs', label: 'Jobs', icon: 'clock' }, { id: 'partner', label: 'Partner', icon: 'handshake' }, { id: 'outreach', label: 'Outreach', icon: 'envelope-simple' }, { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' }, @@ -94,7 +93,6 @@ window.Page_admin = (() => { case 'partner': await _renderPartner(el); break; case 'outreach': await _renderOutreach(el); break; case 'audit': await _renderAudit(el); break; - case 'bewerbungen': await _renderBewerbungen(el); break; } } catch (e) { el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.'); @@ -2377,125 +2375,6 @@ window.Page_admin = (() => { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } - // ------------------------------------------------------------------ - // BEWERBUNGEN — Social-Media-Job - // ------------------------------------------------------------------ - async function _renderBewerbungen(el) { - let _statusFilter = 'pending'; - - async function _load() { - el.innerHTML = ` -
- ${['pending','reviewing','accepted','rejected','alle'].map(s => ` - `).join('')} -
-
${UI.skeleton(3)}
`; - - el.querySelectorAll('.adm-bew-filter').forEach(btn => { - btn.addEventListener('click', () => { - _statusFilter = btn.dataset.s; - _load(); - }); - }); - - try { - const rows = await API.get(`/jobs/admin/applications?status=${_statusFilter}`); - const list = el.querySelector('#adm-bew-list'); - if (!rows.length) { - list.innerHTML = _emptyState('user-plus', 'Keine Bewerbungen', 'Noch keine Bewerbungen in diesem Status.'); - return; - } - list.innerHTML = rows.map(r => ` -
-
-
-
${_esc(r.name)} - ${r.username ? `(@${_esc(r.username)})` : ''} -
-
- ${_esc(r.email)} · @${_esc(r.social_handle||'—')} - ${r.dog_name ? ` · 🐕 ${_esc(r.dog_name)} (${_esc(r.dog_rasse||'')})` : ''} -
-
- ${r.created_at?.slice(0,16).replace('T',' ')} · ${r.doc_count} Anhang/Anhänge -
-
- ${_esc((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''} -
-
-
- - -
-
-
`).join(''); - - list.querySelectorAll('.adm-bew-status').forEach(sel => { - sel.addEventListener('change', async () => { - const id = sel.dataset.id; - await API.patch(`/jobs/admin/applications/${id}`, { status: sel.value }); - UI.toast.success('Status aktualisiert.'); - setTimeout(_load, 500); - }); - }); - - list.querySelectorAll('.adm-bew-view').forEach(btn => { - btn.addEventListener('click', async () => { - const id = btn.dataset.id; - const app = await API.get(`/jobs/admin/applications/${id}`); - const docsHtml = app.docs?.length - ? app.docs.map(d => ` - 📎 ${_esc(d.filename)}`).join('') - : 'Keine Anhänge'; - - UI.modal.open({ - title: `Bewerbung — ${_esc(app.name)}`, - body: ` -
-
E-Mail: ${_esc(app.email)}
-
Social: @${_esc(app.social_handle||'—')}
- ${app.dog_name ? `
Hund: ${_esc(app.dog_name)} (${_esc(app.dog_rasse||'')})
` : ''} -
Motivation:
-
${_esc(app.motivation)}
-
-
Anhänge:
${docsHtml}
-
- Admin-Notiz: - -
-
`, - footer: ` - - `, - }); - document.getElementById('adm-bew-save-note')?.addEventListener('click', async () => { - const note = document.getElementById('adm-bew-note')?.value || ''; - await API.patch(`/jobs/admin/applications/${id}`, { admin_note: note }); - UI.toast.success('Notiz gespeichert.'); - UI.modal.close(); - }); - }); - }); - } catch (e) { - el.querySelector('#adm-bew-list').innerHTML = _emptyState('warning', 'Fehler', e.message); - } - } - - await _load(); - } - // ------------------------------------------------------------------ return { init, refresh, onDogChange }; diff --git a/backend/static/js/pages/jobs.js b/backend/static/js/pages/jobs.js deleted file mode 100644 index 7ad5e68..0000000 --- a/backend/static/js/pages/jobs.js +++ /dev/null @@ -1,260 +0,0 @@ -/* ============================================================ - BAN YARO — Social-Media-Job Bewerbung - ============================================================ */ - -window.Page_jobs = (() => { - - let _container = null; - let _appState = null; - - async function init(container, appState) { - _container = container; - _appState = appState; - await _render(); - } - - async function _render() { - // Bestehende Bewerbung prüfen (nur wenn eingeloggt) - let existingApp = null; - let trialStatus = null; - if (_appState.user) { - try { - const r = await API.get('/jobs/my-application'); - existingApp = r.application; - trialStatus = await API.get('/jobs/luna-trial-status'); - } catch { /* ignorieren */ } - } - - _container.innerHTML = ` -
- - -
-
🐾
-

- Social-Media-Manager/in gesucht -

-

- Werde das Gesicht von Ban Yaro auf Instagram & TikTok -

-
- - -
-
-

Die Stelle

-
- ${_infoRow('📍', 'Remote', '100 % flexibel — du arbeitest wann und wie du willst')} - ${_infoRow('📅', 'Umfang', '1–2 Posts pro Woche auf Instagram & TikTok')} - ${_infoRow('💶', 'Vergütung', '50 € / Monat — wächst mit der Community')} - ${_infoRow('🤖', 'Luna an deiner Seite', 'Unser KI-Assistent schreibt Captions, generiert Post-Ideen und Hashtags — du wählst aus und postest')} - ${_infoRow('⭐', 'Gründer-Status', 'Du wirst Teil der ersten 100 Gründer — für immer')} -
-
-
- - -
-
- 🤖 Luna 14 Tage kostenlos testen -
-

- Mit deiner Bewerbung schalten wir dir sofort den vollen Zugang zu Luna frei — - unserem KI-Assistenten für Social-Media-Posts. Probiere ihn einfach aus, - bevor du dich entscheidest. -

- ${trialStatus?.active ? `
- ✅ Dein Probezugang läuft noch ${trialStatus.remaining_days} Tage
` : ''} -
- - -
-
-

Wen wir suchen

-
    -
  • Du hast einen Hund — und liebst ihn sehr 🐕
  • -
  • Du bist auf Instagram oder TikTok zuhause (nicht professionell, aber aktiv)
  • -
  • Du schreibst gerne und authentisch auf Deutsch
  • -
  • Du hast Lust, eine junge App bekannt zu machen — aus Überzeugung
  • -
  • Kein Lebenslauf nötig. Kein Bewerbungs-Anschreiben. Einfach du.
  • -
-
-
- - - ${existingApp ? _renderStatus(existingApp) : _renderForm()} - -
- `; - - if (!existingApp) { - _bindForm(); - } - } - - function _infoRow(icon, label, text) { - return ` -
- ${icon} -
-
${label}
-
${text}
-
-
`; - } - - function _renderStatus(app) { - const statusMap = { - pending: { icon: '⏳', text: 'Deine Bewerbung ist eingegangen — wir melden uns bald!', color: 'var(--c-text-secondary)' }, - reviewing: { icon: '🔍', text: 'Wir schauen uns deine Bewerbung gerade genauer an.', color: '#0284c7' }, - accepted: { icon: '🎉', text: 'Herzlichen Glückwunsch — du bist dabei!', color: 'var(--c-success)' }, - rejected: { icon: '😔', text: 'Es hat diesmal leider nicht geklappt.', color: 'var(--c-danger)' }, - }; - const s = statusMap[app.status] || statusMap.pending; - return ` -
-
${s.icon}
-
${s.text}
-
- Bewerbung eingereicht: ${app.created_at?.slice(0,10)} -
- ${app.admin_note ? `
${UI.esc(app.admin_note)}
` : ''} -
`; - } - - function _renderForm() { - const u = _appState.user; - return ` -
-
-

- Jetzt bewerben -

-
- -
- - -
- -
- - -
- -
-
- - -
-
- - -
-
- -
- -
- @ - -
-

- Dein öffentliches Profil auf Instagram oder TikTok -

-
- -
- - -

- Mindestens 80 Zeichen -

-
- -
- - -

- Beispiel-Posts, Portfolio, kurzes Video von dir und deinem Hund — max. 3 Dateien, je 10 MB. - PDF, Bild oder Video. -

-
- - ${!u ? `
- 💡 Tipp: Wenn du dich vorher - anmeldest oder registrierst, - bekommst du sofort den 14-tägigen Luna-Probezugang. -
` : ''} - - - -
-
-
`; - } - - function _bindForm() { - document.getElementById('jobs-login-link')?.addEventListener('click', e => { - e.preventDefault(); - if (window.App) App.navigate('settings'); - }); - - document.getElementById('jobs-form')?.addEventListener('submit', async e => { - e.preventDefault(); - const btn = e.target.querySelector('[type="submit"]'); - const fd = new FormData(e.target); - - // Dateien aus file-input übernehmen - const fileInput = document.getElementById('jobs-files'); - if (fileInput?.files?.length) { - fd.delete('files'); - for (const f of fileInput.files) fd.append('files', f); - } - - await UI.asyncButton(btn, async () => { - const resp = await fetch('/api/jobs/apply', { - method: 'POST', - body: fd, - headers: { 'Authorization': `Bearer ${localStorage.getItem('by_token') || ''}` }, - credentials: 'include', - }); - if (!resp.ok) { - const err = await resp.json().catch(() => ({})); - throw new Error(err.detail || 'Fehler beim Absenden.'); - } - const result = await resp.json(); - - if (result.luna_trial) { - UI.toast.success('🎉 Bewerbung eingegangen! Dein Luna-Probezugang ist jetzt aktiv.'); - // User-State aktualisieren damit Luna sofort zugänglich ist - if (_appState.user && window.API) { - try { _appState.user = await API.auth.me(); } catch { /* ignore */ } - } - } else { - UI.toast.success('Bewerbung eingegangen! Wir melden uns bald.'); - } - - await _render(); - }); - }); - } - - return { init }; -})(); diff --git a/backend/static/js/pages/movies.js b/backend/static/js/pages/movies.js index 5f872cb..441a0df 100644 --- a/backend/static/js/pages/movies.js +++ b/backend/static/js/pages/movies.js @@ -13,8 +13,6 @@ window.Page_movies = (() => { let _filme = []; let _activeTab = 'filme'; let _filter = 'alle'; - let _typ = 'alle'; // alle | film | serie | doku - let _sort = 'default'; // default | titel | jahr_desc | jahr_asc | imdb | bewertung // ---------------------------------------------------------- // INIT @@ -72,44 +70,20 @@ window.Page_movies = (() => { // ---------------------------------------------------------- // TAB 1: FILME // ---------------------------------------------------------- - async function _loadFilme() { - _filme = await API.get(`/movies/filme?sort=${_sort}&typ=${_typ}`); - } - async function _renderFilme(content) { try { - await _loadFilme(); + _filme = await API.get('/movies/filme'); } catch { content.innerHTML = UI.emptyState({ icon: 'film-slate', title: 'Filme konnten nicht geladen werden', text: 'Bitte versuche es erneut.' }); return; } content.innerHTML = ` -
-
- - - - -
-
- - - - -
-
- - - -
+
+ + + +
`; @@ -123,26 +97,6 @@ window.Page_movies = (() => { }); }); - content.querySelectorAll('.movies-type-btn').forEach(btn => { - btn.addEventListener('click', async () => { - _typ = btn.dataset.typ; - content.querySelectorAll('.movies-type-btn').forEach(b => b.classList.remove('movies-filter-btn--active')); - btn.classList.add('movies-filter-btn--active'); - const grid = content.querySelector('#movie-grid'); - grid.innerHTML = UI.skeleton(3); - await _loadFilme(); - _renderMovieGrid(grid); - }); - }); - - content.querySelector('#movies-sort')?.addEventListener('change', async e => { - _sort = e.target.value; - const grid = content.querySelector('#movie-grid'); - grid.innerHTML = UI.skeleton(3); - await _loadFilme(); - _renderMovieGrid(grid); - }); - _renderMovieGrid(content.querySelector('#movie-grid')); } @@ -152,10 +106,7 @@ window.Page_movies = (() => { if (_filter === 'stirbt') list = list.filter(f => f.stirbt_der_hund); if (_filter === 'ueberlebt') list = list.filter(f => !f.stirbt_der_hund); - if (_filter === 'top') list = list.filter(f => (f.imdb_rating || 0) >= 7.5 || f.bewertung_avg >= 4.0); - - const countEl = document.getElementById('movies-count'); - if (countEl) countEl.textContent = `${list.length} Einträge`; + if (_filter === 'top') list = list.filter(f => f.bewertung_avg >= 4.0); if (list.length === 0) { grid.innerHTML = `
Keine Filme für diesen Filter.
`; @@ -179,24 +130,18 @@ window.Page_movies = (() => { function _movieCard(film) { const stirbt = film.stirbt_der_hund; const tag = stirbt - ? `
Hund stirbt
` - : `
Hund überlebt
`; + ? `
ACHTUNG: Der Hund stirbt
` + : `
Der Hund überlebt
`; const stars = _starsHtml(film.bewertung_avg, film.id, film.user_rating, false); - const typLabel = film.typ === 'serie' ? '📺 Serie' : film.typ === 'doku' ? '🎥 Doku' : ''; - const imdb = film.imdb_rating ? `IMDb ${film.imdb_rating}` : ''; - const streaming = film.streaming ? `${_esc(film.streaming)}` : ''; return `
${film.bild_emoji}
${_esc(film.titel)} (${film.jahr})
-
- ${_esc(film.genre)}${typLabel ? `${typLabel}` : ''} -
+
${_esc(film.genre)}
${_esc(film.hund_rasse)}
${tag} -
${imdb}${streaming}
${stars}