Feature: Partner-Codes + Gründer-Lizenz-System für Influencer-Kooperationen
- partner_codes Tabelle: Code, Label, max_uses, grants_founder, uses-Counter
- users: is_founder + is_partner Flags (DB-Migration + auth.py SELECT)
- Registrierung: Partner-Code löst Gründer-Lizenz aus (vor User-Referral geprüft)
- API: GET/POST/DELETE /api/admin/partner/codes, POST /api/admin/partner/users/{id}/grant
- API: GET /api/partner/codes/{code}/info (öffentlich, für Registrierungsvalidierung)
- API: GET /api/admin/users/search (Name-Suche für Admin-UI)
- Admin-Tab "Partner & Codes": Code anlegen, Stats, User-Status vergeben
- Registrierungsformular: Einladungscode-Feld mit Live-Validierung
- Settings: Gründer (lila) + Partner (blau) Badge neben Kostenlos/Plus
- SW by-v515, APP_VER 492
This commit is contained in:
parent
810c1a79dc
commit
e57c6db013
9 changed files with 469 additions and 20 deletions
|
|
@ -124,14 +124,21 @@ window.Page_settings = (() => {
|
|||
<div>
|
||||
<div style="font-weight:700;font-size:var(--text-lg)">${_esc(u.name)}</div>
|
||||
<div style="color:var(--c-text-secondary);font-size:var(--text-sm)">${_esc(u.email)}</div>
|
||||
${u.is_premium
|
||||
? `<span class="badge badge-primary" style="margin-top:var(--space-1)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#star"></use></svg> Ban Yaro Plus
|
||||
</span>`
|
||||
: `<span class="badge" style="margin-top:var(--space-1);
|
||||
color:var(--c-text-secondary)">
|
||||
Kostenlos
|
||||
</span>`}
|
||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-1);margin-top:var(--space-1)">
|
||||
${u.is_premium
|
||||
? `<span class="badge badge-primary">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#star"></use></svg> Ban Yaro Plus
|
||||
</span>`
|
||||
: `<span class="badge" style="color:var(--c-text-secondary)">Kostenlos</span>`}
|
||||
${u.is_founder
|
||||
? `<span class="badge" style="background:#7c3aed;color:#fff">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#key"></use></svg> Gründer
|
||||
</span>` : ''}
|
||||
${u.is_partner
|
||||
? `<span class="badge" style="background:#0ea5e9;color:#fff">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#handshake"></use></svg> Partner
|
||||
</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1283,6 +1290,16 @@ window.Page_settings = (() => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:var(--space-2)">
|
||||
<label class="form-label" style="font-size:var(--text-xs)">
|
||||
Einladungscode <span style="color:var(--c-text-muted);font-weight:400">(optional)</span>
|
||||
</label>
|
||||
<input class="form-control" type="text" name="partner_code" id="reg-partner-code"
|
||||
placeholder="z. B. HUNDEBLOG" autocomplete="off"
|
||||
style="text-transform:uppercase;font-family:monospace;letter-spacing:.08em">
|
||||
<div id="reg-partner-hint" style="display:none;margin-top:var(--space-1);font-size:var(--text-xs);
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm)"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2)">
|
||||
Konto erstellen
|
||||
</button>
|
||||
|
|
@ -1375,6 +1392,44 @@ 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"]');
|
||||
|
|
@ -1390,8 +1445,10 @@ window.Page_settings = (() => {
|
|||
}
|
||||
|
||||
await UI.asyncButton(btn, async () => {
|
||||
const refCode = sessionStorage.getItem('by_ref_code') || '';
|
||||
const result = await API.auth.register(fd.email, fd.password, fd.name.trim(), refCode || undefined);
|
||||
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);
|
||||
localStorage.setItem('by_token', result.token);
|
||||
if (refCode) sessionStorage.removeItem('by_ref_code');
|
||||
|
||||
|
|
@ -1401,7 +1458,10 @@ window.Page_settings = (() => {
|
|||
_appState.activeDog = null;
|
||||
|
||||
document.getElementById('header-login-btn')?.remove();
|
||||
UI.toast.success(`Willkommen bei Ban Yaro, ${_appState.user.name}!`);
|
||||
const greeting = _appState.user.is_founder
|
||||
? `Willkommen, Gründer ${_appState.user.name}! 🎉`
|
||||
: `Willkommen bei Ban Yaro, ${_appState.user.name}!`;
|
||||
UI.toast.success(greeting);
|
||||
App.showOnboarding();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue