diff --git a/backend/auth.py b/backend/auth.py index d923c70..b3365aa 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 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 FROM users WHERE id=?", (user_id,) ).fetchone() diff --git a/backend/database.py b/backend/database.py index e428a56..940c76f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -559,10 +559,6 @@ def _migrate(conn_factory): ("users", "ki_zucht_paarung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_beschreibung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_jahresbericht", "INTEGER NOT NULL DEFAULT 1"), - # Partner-Code + Gründer-Lizenz - ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"), - ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"), - ("users", "founder_number", "INTEGER"), ] with conn_factory() as conn: for table, column, col_type in migrations: @@ -1489,25 +1485,6 @@ def _migrate(conn_factory): CREATE INDEX IF NOT EXISTS idx_bj_user ON breeder_jahresberichte(user_id, jahr DESC); """) - # Partner-Codes (Influencer-Kooperationen + Gründer-Lizenz) - try: - conn.executescript(""" - CREATE TABLE IF NOT EXISTS partner_codes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - code TEXT NOT NULL UNIQUE COLLATE NOCASE, - label TEXT NOT NULL, - grants_founder INTEGER NOT NULL DEFAULT 1, - max_uses INTEGER DEFAULT NULL, - uses INTEGER NOT NULL DEFAULT 0, - created_by INTEGER REFERENCES users(id), - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_partner_codes_code ON partner_codes(code); - """) - logger.info("Migration: partner_codes Tabelle bereit.") - except Exception as e: - logger.warning(f"Migration partner_codes: {e}") - # 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 db883dc..4490cbb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -162,7 +162,6 @@ from routes.breeder_photos import router as breeder_photos_router from routes.zucht_hunde import router as zucht_hunde_router 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 app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -194,7 +193,6 @@ app.include_router(breeder_photos_router, prefix="/api", tags=["Züchter- app.include_router(zucht_hunde_router, prefix="/api", tags=["Zuchtkartei"]) 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(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"]) @@ -1403,16 +1401,6 @@ async def knigge_page(): # SPA Fallback — ALLE nicht-API-Routen gehen zur index.html @app.get("/{full_path:path}") async def spa_fallback(full_path: str): - IS_STAGING = os.getenv("STAGING", "false").lower() == "true" - if IS_STAGING: - from fastapi.responses import HTMLResponse - with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as f: - html = f.read() - html = html.replace( - '', - '', - ) - return HTMLResponse(content=html, headers={"Cache-Control": "no-cache"}) return FileResponse( f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"} diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 32d5c6c..73349b4 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -78,42 +78,13 @@ async def register(data: RegisterRequest, response: Response, request: Request): new_user_id = user["id"] if data.ref_code: - code_upper = data.ref_code.strip().upper() - # Zuerst prüfen ob es ein Partner-Code ist - partner = conn.execute( - "SELECT id, grants_founder, max_uses, uses FROM partner_codes WHERE code=?", - (code_upper,) + referrer = conn.execute( + "SELECT id FROM users WHERE referral_code=? AND id != ?", + (data.ref_code.strip().upper(), new_user_id) ).fetchone() - if partner: - # Nur einlösen wenn max_uses nicht erreicht - if partner["max_uses"] is None or partner["uses"] < partner["max_uses"]: - conn.execute( - "UPDATE partner_codes SET uses=uses+1 WHERE id=?", - (partner["id"],) - ) - updates = {"referred_by": -partner["id"]} - if partner["grants_founder"]: - total_founders = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if total_founders < 100: - founder_num = total_founders + 1 - updates["is_founder"] = 1 - updates["founder_number"] = founder_num - set_clause = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE users SET {set_clause} WHERE id=?", - (*updates.values(), new_user_id) - ) - else: - # Fallback: Referral-Code eines anderen Users - referrer = conn.execute( - "SELECT id FROM users WHERE referral_code=? AND id != ?", - (code_upper, new_user_id) - ).fetchone() - if referrer: - conn.execute("UPDATE users SET referred_by=? WHERE id=?", - (referrer['id'], new_user_id)) + if referrer: + conn.execute("UPDATE users SET referred_by=? WHERE id=?", + (referrer['id'], new_user_id)) token = create_token(user["id"], user["rolle"]) _set_cookie(response, token) @@ -149,24 +120,6 @@ async def logout(response: Response): return {"ok": True} -_REFERRAL_TIERS = [ - (50, 50), - (20, 30), - (10, 20), -] - -def _referral_tier(count: int): - for threshold, discount in _REFERRAL_TIERS: - if count >= threshold: - return discount - return 0 - -def _referral_next(count: int): - for threshold, discount in reversed(_REFERRAL_TIERS): - if count < threshold: - return {"count": threshold, "discount": discount} - return None # Maximalstufe erreicht - @router.get("/referral") async def get_referral_info(user=Depends(get_current_user)): with db() as conn: @@ -180,14 +133,11 @@ async def get_referral_info(user=Depends(get_current_user)): if not code: code = _gen_referral_code() conn.execute("UPDATE users SET referral_code=? WHERE id=?", (code, user['id'])) - count = row["count"] if row else 0 - base = os.getenv("APP_URL", "https://banyaro.app") + base = os.getenv("APP_URL", "https://banyaro.app") return { - "code": code, - "count": count, - "link": f"{base}/?ref={code}", - "discount_pct": _referral_tier(count), - "next_tier": _referral_next(count), + "code": code, + "count": row["count"] if row else 0, + "link": f"{base}/?ref={code}", } diff --git a/backend/routes/forum.py b/backend/routes/forum.py index b6d204f..d22a460 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -131,7 +131,7 @@ async def list_threads( t.antworten, t.likes, t.views, t.is_pinned, t.is_locked, t.foto_urls, t.created_at, t.user_id, - u.name AS autor_name, u.founder_number AS autor_founder_number + u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.is_deleted = 0 @@ -183,7 +183,7 @@ async def create_thread(data: ThreadCreate, user=Depends(get_current_user)): data.thread_lat, data.thread_lon, data.thread_ort, ct) ) row = conn.execute( - """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT t.*, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.id = ?""", @@ -203,7 +203,7 @@ async def get_thread(thread_id: int, user=Depends(get_current_user_optional)): uid = user['id'] if user else None with db() as conn: thread = conn.execute( - """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT t.*, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.id = ? AND t.is_deleted = 0""", @@ -218,7 +218,7 @@ async def get_thread(thread_id: int, user=Depends(get_current_user_optional)): ) posts = conn.execute( - """SELECT p.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT p.*, u.name AS autor_name FROM forum_posts p LEFT JOIN users u ON u.id = p.user_id WHERE p.thread_id = ? @@ -288,7 +288,7 @@ async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_curre [*updates.values(), thread_id] ) row = conn.execute( - """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT t.*, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.id = ?""", @@ -328,7 +328,7 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current (thread_id,) ) row = conn.execute( - """SELECT p.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT p.*, u.name AS autor_name FROM forum_posts p LEFT JOIN users u ON u.id = p.user_id WHERE p.id = ?""", diff --git a/backend/routes/partner.py b/backend/routes/partner.py deleted file mode 100644 index 745fde7..0000000 --- a/backend/routes/partner.py +++ /dev/null @@ -1,192 +0,0 @@ -"""BAN YARO — Partner-Codes + Gründer-Lizenz""" - -from typing import Optional -from fastapi import APIRouter, HTTPException, Depends -from pydantic import BaseModel -from database import db -from auth import require_admin, get_current_user - -router = APIRouter() - - -class PartnerCodeCreate(BaseModel): - code: str - label: str - grants_founder: int = 1 - max_uses: Optional[int] = None - - -class GrantRequest(BaseModel): - is_founder: Optional[int] = None - is_partner: Optional[int] = None - - -# ------------------------------------------------------------------ -# Admin: Partner-Codes verwalten -# ------------------------------------------------------------------ - -@router.get("/admin/partner/codes") -def list_partner_codes(user=Depends(require_admin)): - """Alle Partner-Codes mit Stats (admin only).""" - with db() as conn: - rows = conn.execute( - """SELECT pc.id, pc.code, pc.label, pc.grants_founder, - pc.max_uses, pc.uses, pc.created_at, - u.name AS created_by_name - FROM partner_codes pc - LEFT JOIN users u ON u.id = pc.created_by - ORDER BY pc.created_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -@router.post("/admin/partner/codes", status_code=201) -def create_partner_code(data: PartnerCodeCreate, user=Depends(require_admin)): - """Neuen Partner-Code erstellen (admin only).""" - code = data.code.strip().upper() - if not code: - raise HTTPException(400, "Code darf nicht leer sein.") - with db() as conn: - existing = conn.execute( - "SELECT id FROM partner_codes WHERE code=?", (code,) - ).fetchone() - if existing: - raise HTTPException(400, "Dieser Code existiert bereits.") - conn.execute( - """INSERT INTO partner_codes (code, label, grants_founder, max_uses, created_by) - VALUES (?, ?, ?, ?, ?)""", - (code, data.label.strip(), data.grants_founder, data.max_uses, user["id"]) - ) - row = conn.execute( - "SELECT * FROM partner_codes WHERE code=?", (code,) - ).fetchone() - return dict(row) - - -@router.delete("/admin/partner/codes/{code_id}", status_code=204) -def delete_partner_code(code_id: int, user=Depends(require_admin)): - """Partner-Code löschen (admin only).""" - with db() as conn: - existing = conn.execute( - "SELECT id FROM partner_codes WHERE id=?", (code_id,) - ).fetchone() - if not existing: - raise HTTPException(404, "Partner-Code nicht gefunden.") - conn.execute("DELETE FROM partner_codes WHERE id=?", (code_id,)) - return None - - -@router.post("/admin/partner/users/{user_id}/grant") -def grant_user_status(user_id: int, data: GrantRequest, user=Depends(require_admin)): - """Founder- und/oder Partner-Status für einen User setzen (admin only).""" - updates = {} - if data.is_founder is not None: - updates["is_founder"] = data.is_founder - if data.is_partner is not None: - updates["is_partner"] = data.is_partner - if not updates: - raise HTTPException(400, "Mindestens is_founder oder is_partner muss angegeben werden.") - with db() as conn: - target = conn.execute( - "SELECT id, is_founder, founder_number FROM users WHERE id=?", (user_id,) - ).fetchone() - if not target: - raise HTTPException(404, "User nicht gefunden.") - # Beim manuellen Vergeben von is_founder: founder_number zuweisen wenn noch keine - if updates.get("is_founder") == 1 and not target["founder_number"]: - total = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if total >= FOUNDER_MAX: - raise HTTPException(400, f"Alle {FOUNDER_MAX} Gründer-Plätze sind vergeben.") - updates["founder_number"] = total + 1 - set_clause = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE users SET {set_clause} WHERE id=?", - (*updates.values(), user_id) - ) - row = conn.execute( - "SELECT id, name, email, is_founder, is_partner, founder_number FROM users WHERE id=?", - (user_id,) - ).fetchone() - return dict(row) - - -@router.get("/admin/users/search") -def search_users(q: str, user=Depends(require_admin)): - """User-Suche für Admin (Name-Präfix, max. 10 Ergebnisse).""" - with db() as conn: - rows = conn.execute( - """SELECT id, name, email, is_founder, is_partner, rolle - FROM users WHERE name LIKE ? COLLATE NOCASE - ORDER BY name LIMIT 10""", - (f"{q}%",) - ).fetchall() - return [dict(r) for r in rows] - - -# ------------------------------------------------------------------ -# Öffentlich: Gründer-Leaderboard + Code-Info -# ------------------------------------------------------------------ - -FOUNDER_MAX = 100 - -@router.get("/partner/founders/stats") -def founders_stats(): - """Öffentliches Gründer-Leaderboard: Slots, Partner-Ranking, Gründer-Liste.""" - with db() as conn: - total = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - # Partner-Ranking: nach uses absteigend - partners = conn.execute( - """SELECT pc.id, pc.label, pc.uses, pc.code, - u.name AS partner_username, u.is_partner - FROM partner_codes pc - LEFT JOIN users u ON u.referred_by = -pc.id AND u.is_partner=1 - WHERE pc.grants_founder=1 - ORDER BY pc.uses DESC""" - ).fetchall() - # Erste 100 Gründer (für Galerie) - founders = conn.execute( - """SELECT u.name, u.founder_number, - pc.label AS via_partner - FROM users u - LEFT JOIN partner_codes pc ON u.referred_by = -pc.id - WHERE u.is_founder=1 AND u.founder_number IS NOT NULL - ORDER BY u.founder_number ASC - LIMIT 100""" - ).fetchall() - return { - "total": total, - "max": FOUNDER_MAX, - "open": max(0, FOUNDER_MAX - total), - "closed": total >= FOUNDER_MAX, - "partners": [dict(p) for p in partners], - "founders": [dict(f) for f in founders], - } - - -@router.get("/partner/codes/{code}/info") -def partner_code_info(code: str): - """Gibt zurück ob ein Partner-Code existiert und dessen Label (öffentlich).""" - with db() as conn: - row = conn.execute( - """SELECT code, label, grants_founder, max_uses, uses - FROM partner_codes WHERE code=?""", - (code.strip().upper(),) - ).fetchone() - if not row: - raise HTTPException(404, "Partner-Code nicht gefunden.") - r = dict(row) - if r["grants_founder"]: - with db() as conn2: - total = conn2.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - r["founder_slots_open"] = max(0, FOUNDER_MAX - total) - r["redeemable"] = (r["max_uses"] is None or r["uses"] < r["max_uses"]) and total < FOUNDER_MAX - else: - r["founder_slots_open"] = None - r["redeemable"] = r["max_uses"] is None or r["uses"] < r["max_uses"] - return r diff --git a/backend/static/icons/icon-180-staging.png b/backend/static/icons/icon-180-staging.png deleted file mode 100644 index fc2be5c..0000000 Binary files a/backend/static/icons/icon-180-staging.png and /dev/null differ diff --git a/backend/static/index.html b/backend/static/index.html index 25a3ee5..3f4acbb 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -230,7 +230,6 @@ border-top:1px solid var(--c-border,#e5e7eb); font-size:var(--text-xs);color:var(--c-text-muted); display:flex;gap:var(--space-3);padding-bottom:var(--space-2)"> - 🏆 100 Gründer Impressum Datenschutz @@ -420,10 +419,6 @@
-
-
-
- diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 27a2a8a..697e7d6 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '495'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '490'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.0.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; @@ -69,7 +69,6 @@ const App = (() => { wurfboerse: { title: 'Wurfbörse', module: null }, zuchthunde: { title: 'Zuchtkartei', module: null, requiresAuth: true }, 'zucht-profil': { title: 'Hunde-Profil', module: null }, - gruender: { title: '100 Gründer', module: null }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 4706cee..9452095 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -19,7 +19,6 @@ window.Page_admin = (() => { { id: 'analytics', label: 'Analytics', icon: 'target' }, { id: 'system', label: 'System', icon: 'gear' }, { id: 'jobs', label: 'Jobs', icon: 'clock' }, - { id: 'partner', label: 'Partner & Codes', icon: 'handshake' }, { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' }, ]; @@ -89,7 +88,6 @@ window.Page_admin = (() => { case 'analytics': await _renderAnalytics(el); break; case 'system': await _renderSystem(el); break; case 'jobs': await _renderJobs(el); break; - case 'partner': await _renderPartner(el); break; case 'audit': await _renderAudit(el); break; } } catch (e) { @@ -1791,210 +1789,6 @@ window.Page_admin = (() => { // ------------------------------------------------------------------ // TAB: AUDIT-LOG // ------------------------------------------------------------------ - async function _renderPartner(el) { - const [codes] = await Promise.all([ - API.get('/api/admin/partner/codes'), - ]); - - el.innerHTML = ` -
- - -
-

Neuen Partner-Code erstellen

-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- -
-
- - -
-

Aktive Codes

-
- ${codes.length === 0 - ? `

Noch keine Partner-Codes angelegt.

` - : ` - - - - - - - - - - - ${codes.map(c => ` - - - - - - - - `).join('')} - -
CodeBezeichnungNutzungenGründer
- ${c.code} - ${c.label} - ${c.uses}${c.max_uses ? `/${c.max_uses}` : ''} - - ${c.grants_founder ? '✓' : '—'} - - -
` - } -
-
- - -
-

Nutzer-Status manuell vergeben

-
-
- - -
-
-
- - -
- -
-
- -
- `; - - // Code erstellen - el.querySelector('#adm-partner-create')?.addEventListener('submit', async e => { - e.preventDefault(); - const btn = e.target.querySelector('[type="submit"]'); - const fd = UI.formData(e.target); - const code = (fd.code || '').trim().toUpperCase(); - if (!code) return; - await UI.asyncButton(btn, async () => { - await API.post('/api/admin/partner/codes', { - code, - label: fd.label || code, - grants_founder: e.target.querySelector('[name="grants_founder"]').checked ? 1 : 0, - max_uses: fd.max_uses ? parseInt(fd.max_uses) : null, - }); - UI.toast.success(`Code "${code}" erstellt.`); - await _renderPartner(el); - }); - }); - - // Code löschen - el.querySelectorAll('.adm-del-code').forEach(btn => { - btn.addEventListener('click', async () => { - if (!window.confirm(`Code wirklich löschen?`)) return; - const id = btn.dataset.id; - await UI.asyncButton(btn, async () => { - await API.del(`/api/admin/partner/codes/${id}`); - UI.toast.success('Code gelöscht.'); - await _renderPartner(el); - }); - }); - }); - - // User suchen für Status-Vergabe - let _grantUserId = null; - const searchInput = el.querySelector('#adm-grant-search'); - const grantResult = el.querySelector('#adm-grant-result'); - let _searchTimeout = null; - searchInput?.addEventListener('input', () => { - clearTimeout(_searchTimeout); - _grantUserId = null; - const q = searchInput.value.trim(); - if (q.length < 2) { grantResult.innerHTML = ''; return; } - _searchTimeout = setTimeout(async () => { - try { - const users = await API.get(`/api/admin/users/search?q=${encodeURIComponent(q)}`); - if (!users.length) { - grantResult.innerHTML = `

Kein User gefunden.

`; - return; - } - grantResult.innerHTML = users.map(u => ` -
- ${u.name} - - ${u.is_founder ? '⭐ Gründer ' : ''}${u.is_partner ? '🤝 Partner' : ''} - -
- `).join(''); - grantResult.querySelectorAll('.adm-grant-user').forEach(div => { - div.addEventListener('click', () => { - _grantUserId = parseInt(div.dataset.id); - searchInput.value = div.dataset.name; - grantResult.innerHTML = `

✓ ${div.dataset.name} ausgewählt

`; - }); - }); - } catch { grantResult.innerHTML = ''; } - }, 400); - }); - - el.querySelector('#adm-partner-grant')?.addEventListener('submit', async e => { - e.preventDefault(); - if (!_grantUserId) { UI.toast.warning('Bitte erst einen User auswählen.'); return; } - const btn = e.target.querySelector('[type="submit"]'); - const isFounder = e.target.querySelector('[name="is_founder"]').checked ? 1 : 0; - const isPartner = e.target.querySelector('[name="is_partner"]').checked ? 1 : 0; - await UI.asyncButton(btn, async () => { - const result = await API.post(`/api/admin/partner/users/${_grantUserId}/grant`, { - is_founder: isFounder, - is_partner: isPartner, - }); - UI.toast.success(`Status für ${result.name} gesetzt.`); - grantResult.innerHTML = `

✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'}

`; - }); - }); - } - async function _renderAudit(el) { el.innerHTML = `
diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index f29ff18..c46fe6a 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -392,7 +392,6 @@ window.Page_forum = (() => {
${_esc(_initial(thread.autor_name))}
${_esc(thread.autor_name || 'Unbekannt')} - ${thread.autor_founder_number ? `Gründer #${thread.autor_founder_number}` : ''}
@@ -307,9 +299,9 @@ window.Page_settings = (() => {
-
${UI.icon('arrow-square-out')} Freunde werben — dauerhafter Rabatt
+
${UI.icon('arrow-square-out')} App empfehlen
- 10 Freunde → 20% · 20 Freunde → 30% · 50 Freunde → 50% — lebenslang, sobald Bezahlfunktionen aktiv sind. + Lade Freunde ein — jede erfolgreiche Einladung wird in deinem Profil angezeigt.
Lade…
@@ -1100,88 +1092,50 @@ window.Page_settings = (() => { if (!el) return; try { const r = await API.auth.referral(); - - const TIERS = [{t:10,d:20},{t:20,d:30},{t:50,d:50}]; - const currentTier = r.discount_pct; - const next = r.next_tier; - - // Fortschrittsbalken-Berechnung - let barPct = 0, barLabel = ''; - if (!next) { - barPct = 100; - barLabel = 'Maximaler Rabatt erreicht!'; - } else { - const prevT = TIERS.find(t => t.d === currentTier)?.t || 0; - barPct = Math.round(((r.count - prevT) / (next.count - prevT)) * 100); - barLabel = `Noch ${next.count - r.count} ${next.count - r.count === 1 ? 'Person' : 'Personen'} bis ${next.discount}% Rabatt`; - } - el.innerHTML = ` - -
- ${TIERS.map(({t, d}) => { - const reached = r.count >= t; - return `
-
- ${d}% -
-
ab ${t} Freunden
- ${reached ? `
✓ Erreicht
` : ''} -
`; - }).join('')} -
- - -
-
- - ${UI.icon('users')} ${r.count} ${r.count === 1 ? 'Freund geworben' : 'Freunde geworben'} - - ${currentTier > 0 ? `${currentTier}% Rabatt aktiv` : ''} -
-
-
-
-
${barLabel}
-
- -
${r.link}
-
-
-
+ + +
+
+
+ width:36px;height:36px;border-radius:8px;border:2px solid #fff">
-

- Der Rabatt gilt für dich — sobald Bezahlfunktionen aktiv sind, dauerhaft und automatisch. -

- `; +
+ ${UI.icon('users')} ${r.count} ${r.count === 1 ? 'Person' : 'Personen'} über deinen Link registriert +
+ ${r.count > 0 ? ` +
+ ${r.count >= 1 ? `${UI.icon('star')} Botschafter` : ''} + ${r.count >= 5 ? `${UI.icon('star')} Super-Botschafter` : ''} + ${r.count >= 10 ? `${UI.icon('star')} Top-Botschafter` : ''} +
` : ''} + `; document.getElementById('ref-share-btn')?.addEventListener('click', async () => { - const msg = `Ich bin bei Ban Yaro — der coolsten Hunde-App! Registrier dich mit meinem Link und wir wachsen zusammen 🐾`; if (navigator.share) { - navigator.share({ title: 'Ban Yaro', text: msg, url: r.link }).catch(() => {}); + navigator.share({ title: 'Ban Yaro — Die Hunde-App', text: 'Schau dir Ban Yaro an!', url: r.link }).catch(() => {}); } else { await navigator.clipboard.writeText(r.link); UI.toast.success('Link kopiert!'); } }); - + // QR-Code rendern (Bibliothek lazy laden) await App.loadScript('/js/qrcode.min.js'); new QRCode(document.getElementById('ref-qr'), { - text: r.link, width: 140, height: 140, - colorDark: '#000000', colorLight: '#ffffff', + text: r.link, + width: 160, + height: 160, + colorDark: '#000000', + colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H, }); } catch { el.innerHTML = ''; } @@ -1329,16 +1283,6 @@ window.Page_settings = (() => {
-
- - - -
@@ -1431,44 +1375,6 @@ window.Page_settings = (() => { }); } - // Partner-Code live validieren - const partnerInput = document.getElementById('reg-partner-code'); - const partnerHint = document.getElementById('reg-partner-hint'); - let _partnerValid = false; - if (partnerInput) { - // Vorausfüllen falls via sessionStorage gesetzt - const stored = sessionStorage.getItem('by_ref_code') || ''; - if (stored) partnerInput.value = stored; - - let _debounce = null; - partnerInput.addEventListener('input', () => { - const code = partnerInput.value.trim().toUpperCase(); - partnerInput.value = code; - clearTimeout(_debounce); - partnerHint.style.display = 'none'; - _partnerValid = false; - if (code.length < 3) return; - _debounce = setTimeout(async () => { - try { - const info = await API.get(`/api/partner/codes/${encodeURIComponent(code)}/info`); - if (info.redeemable) { - partnerHint.textContent = info.grants_founder - ? `✓ Gültiger Code von "${info.label}" — du erhältst eine lebenslange Gründer-Lizenz!` - : `✓ Gültiger Einladungscode von "${info.label}"`; - partnerHint.style.cssText = 'display:block;background:var(--c-success-bg,#f0fdf4);color:var(--c-success,#16a34a);padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);font-size:var(--text-xs)'; - _partnerValid = true; - } else { - partnerHint.textContent = 'Dieser Code ist bereits vollständig eingelöst.'; - partnerHint.style.cssText = 'display:block;background:#fef2f2;color:#dc2626;padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);font-size:var(--text-xs)'; - } - } catch { - partnerHint.textContent = 'Code nicht gefunden.'; - partnerHint.style.cssText = 'display:block;color:var(--c-text-muted);font-size:var(--text-xs)'; - } - }, 500); - }); - } - document.getElementById('auth-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = e.target.querySelector('[type="submit"]'); @@ -1484,10 +1390,8 @@ window.Page_settings = (() => { } await UI.asyncButton(btn, async () => { - const partnerCode = (fd.partner_code || '').trim().toUpperCase() || undefined; - const refCode = sessionStorage.getItem('by_ref_code') || ''; - const finalCode = partnerCode || refCode || undefined; - const result = await API.auth.register(fd.email, fd.password, fd.name.trim(), finalCode); + const refCode = sessionStorage.getItem('by_ref_code') || ''; + const result = await API.auth.register(fd.email, fd.password, fd.name.trim(), refCode || undefined); localStorage.setItem('by_token', result.token); if (refCode) sessionStorage.removeItem('by_ref_code'); @@ -1497,10 +1401,7 @@ window.Page_settings = (() => { _appState.activeDog = null; document.getElementById('header-login-btn')?.remove(); - const greeting = _appState.user.is_founder - ? `Willkommen, Gründer ${_appState.user.name}! 🎉` - : `Willkommen bei Ban Yaro, ${_appState.user.name}!`; - UI.toast.success(greeting); + UI.toast.success(`Willkommen bei Ban Yaro, ${_appState.user.name}!`); App.showOnboarding(); }); }); diff --git a/backend/static/sw.js b/backend/static/sw.js index ad12985..c28125f 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v518'; +const CACHE_VERSION = 'by-v513'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache