Feature: Referral-System — User wirbt User

- DB: referral_code (8-stellig, eindeutig) + referred_by zu users
  Bestehende User erhalten automatisch einen Code
- GET /api/auth/referral: Code, Link und Anzahl geworbener User
- POST /api/auth/register: ref_code Parameter für Zuordnung
- Settings: 'App empfehlen'-Karte mit Link, Teilen-Button und Botschafter-Badges
  (Botschafter ab 1, Super ab 5, Top ab 10 Einladungen)
- app.js: ?ref=CODE aus URL in sessionStorage speichern
- APP_VER 222, SW by-v244
This commit is contained in:
rene 2026-04-19 11:09:24 +02:00
parent 82d9e26823
commit 6d757b86c2
6 changed files with 121 additions and 7 deletions

View file

@ -78,8 +78,10 @@ const API = (() => {
login(email, password) {
return post('/auth/login', { email, password });
},
register(email, password, name) {
return post('/auth/register', { email, password, name });
register(email, password, name, ref_code) {
const body = { email, password, name };
if (ref_code) body.ref_code = ref_code;
return post('/auth/register', body);
},
logout() {
localStorage.removeItem('by_token');
@ -88,6 +90,7 @@ const API = (() => {
me() {
return get('/auth/me');
},
referral: () => get('/auth/referral'),
};
// ----------------------------------------------------------

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '221'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '222'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {
@ -687,6 +687,15 @@ const App = (() => {
// INITIALISIERUNG
// ----------------------------------------------------------
async function init() {
// Referral-Code aus URL ?ref=CODE speichern
const urlParams = new URLSearchParams(window.location.search);
const refCode = urlParams.get('ref');
if (refCode) {
sessionStorage.setItem('by_ref_code', refCode.toUpperCase());
// URL bereinigen ohne Reload
history.replaceState({}, '', window.location.pathname + window.location.hash);
}
_bindNavigation();
await _checkAuth();

View file

@ -227,6 +227,17 @@ window.Page_settings = (() => {
</div>
</div>
<!-- App empfehlen -->
<div class="card" style="margin-bottom:var(--space-5)" id="referral-card">
<div style="padding:var(--space-4);border-bottom:1px solid var(--c-border)">
<div style="font-weight:600;margin-bottom:2px">${UI.icon('arrow-square-out')} App empfehlen</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
Lade Freunde ein jede erfolgreiche Einladung wird in deinem Profil angezeigt.
</div>
</div>
<div id="referral-body" style="padding:var(--space-4)">Lade</div>
</div>
<!-- App installieren -->
<div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4);
@ -470,6 +481,44 @@ window.Page_settings = (() => {
? 'Pocket-Modus aktiviert — Bildschirm bleibt bei Aufzeichnung an.'
: 'Pocket-Modus deaktiviert.');
});
_loadReferral();
}
// ----------------------------------------------------------
// REFERRAL — Einladungslink laden und rendern
// ----------------------------------------------------------
async function _loadReferral() {
const el = document.getElementById('referral-body');
if (!el) return;
try {
const r = await API.auth.referral();
el.innerHTML = `
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
<div style="flex:1;background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-2) var(--space-3);font-family:monospace;font-size:var(--text-sm);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${r.link}</div>
<button class="btn btn-primary btn-sm" id="ref-share-btn">${UI.icon('arrow-square-out')} Teilen</button>
</div>
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
${UI.icon('users')} <strong>${r.count}</strong> ${r.count === 1 ? 'Person' : 'Personen'} über deinen Link registriert
</div>
${r.count > 0 ? `
<div style="margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap">
${r.count >= 1 ? `<span class="badge badge-primary">${UI.icon('star')} Botschafter</span>` : ''}
${r.count >= 5 ? `<span class="badge badge-primary">${UI.icon('star')} Super-Botschafter</span>` : ''}
${r.count >= 10 ? `<span class="badge badge-primary">${UI.icon('star')} Top-Botschafter</span>` : ''}
</div>` : ''}
`;
document.getElementById('ref-share-btn')?.addEventListener('click', async () => {
if (navigator.share) {
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!');
}
});
} catch { el.innerHTML = '<p style="color:var(--c-text-muted)">Nicht verfügbar.</p>'; }
}
// ----------------------------------------------------------
@ -662,8 +711,10 @@ window.Page_settings = (() => {
}
await UI.asyncButton(btn, async () => {
const result = await API.auth.register(fd.email, fd.password, fd.name.trim());
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');
_appState.user = await API.auth.me();
document.getElementById('sidebar-username').textContent = _appState.user.name;

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v242';
const CACHE_VERSION = 'by-v244';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten