banyaro/backend/static/js/pages/jobs.js
rene 55069d246b Feature: Welten-Onboarding, Wetter-Motivation, UX-Fixes (SW by-v715)
Welten (worlds.js):
- Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig)
- Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte
- Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto
- worlds-back: navigiert zu Welcome wenn kein User eingeloggt
- Nach Logout: worlds-back Button sofort ausgeblendet

Wetter (wetter.js):
- Standort-Fehlerseite zu Motivations-Seite umgebaut
- Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde
- CTA: Standort freigeben + Registrieren (nur für Gäste)

Settings (settings.js):
- Logo in Auth-Form: display:block + margin:0 auto zentriert
- Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert)

Jobs (jobs.js):
- 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr
- Kein doppeltes Padding im Wrapper

Backend:
- weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN)
- Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee
- Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen
- Presse-Seite erweitert
- Ban Yaro Foto-Assets (WebP + HIRES JPG)
2026-05-05 17:32:03 +02:00

269 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Social-Media-Job Bewerbung
============================================================ */
window.Page_jobs = (() => {
let _container = null;
let _appState = null;
const _esc = s => UI.escape(s ?? '');
const _ph = (name, size = 22) =>
`<svg class="ph-icon" aria-hidden="true" style="width:${size}px;height:${size}px;flex-shrink:0;color:var(--c-primary)"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
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 = `
<div style="max-width:640px;margin:0 auto;padding:0;box-sizing:border-box">
<!-- Header -->
<div style="text-align:center;margin-bottom:var(--space-6)">
<div style="font-size:48px;margin-bottom:var(--space-3)">🐾</div>
<h1 style="font-size:var(--text-2xl);font-weight:800;margin:0 0 var(--space-2)">
Social-Media-Manager/in gesucht
</h1>
<p style="color:var(--c-text-secondary);margin:0">
Werde das Gesicht von Ban Yaro auf Instagram &amp; TikTok
</p>
</div>
<!-- Stellenbeschreibung -->
<div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-5)">
<h2 style="font-size:var(--text-lg);font-weight:700;margin:0 0 var(--space-4);color:var(--c-primary)">Die Stelle</h2>
<div style="display:grid;gap:var(--space-3)">
${_infoRow(_ph('map-pin'), 'Remote', '100 % flexibel — du arbeitest wann und wie du willst')}
${_infoRow(_ph('calendar-dots'), 'Umfang', '12 Posts pro Woche auf Instagram &amp; TikTok')}
${_infoRow(_ph('tag'), 'Vergütung', '50 € / Monat — wächst mit der Community')}
${_infoRow(_ph('robot'), 'Luna an deiner Seite', 'Unser KI-Assistent schreibt Captions, generiert Post-Ideen und Hashtags — du wählst aus und postest')}
${_infoRow(_ph('star'), 'Gründer-Status', 'Du wirst Teil der ersten 100 Gründer — für immer')}
</div>
</div>
</div>
<!-- Luna-Probezugang Teaser -->
<div style="background:linear-gradient(135deg,var(--c-primary),#e8a857);border-radius:var(--radius-lg);
padding:var(--space-5);margin-bottom:var(--space-4);color:#fff">
<div style="font-size:var(--text-lg);font-weight:800;margin-bottom:var(--space-2);display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true" style="width:24px;height:24px"><use href="/icons/phosphor.svg#robot"></use></svg>
Luna 14 Tage kostenlos testen
</div>
<p style="margin:0;opacity:.9;font-size:var(--text-sm)">
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.
</p>
${trialStatus?.active ? `<div style="margin-top:var(--space-3);background:rgba(255,255,255,.2);
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);font-weight:700;font-size:var(--text-sm)">
<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px;vertical-align:middle"><use href="/icons/phosphor.svg#check-circle"></use></svg>
Dein Probezugang läuft noch ${trialStatus.remaining_days} Tage</div>` : ''}
</div>
<!-- Wen wir suchen -->
<div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-5)">
<h2 style="font-size:var(--text-lg);font-weight:700;margin:0 0 var(--space-4);color:var(--c-primary)">Wen wir suchen</h2>
<ul style="margin:0;padding-left:var(--space-5);display:grid;gap:var(--space-2);color:var(--c-text-secondary);font-size:var(--text-sm)">
<li>Du hast einen Hund — und liebst ihn sehr 🐕</li>
<li>Du bist auf Instagram oder TikTok zuhause (nicht professionell, aber aktiv)</li>
<li>Du schreibst gerne und authentisch auf Deutsch</li>
<li>Du hast Lust, eine junge App bekannt zu machen — aus Überzeugung</li>
<li>Kein Lebenslauf nötig. Kein Bewerbungs-Anschreiben. Einfach du.</li>
</ul>
</div>
</div>
<!-- Bewerbungsformular oder Status -->
${existingApp ? _renderStatus(existingApp) : _renderForm()}
</div>
`;
if (!existingApp) {
_bindForm();
}
}
function _infoRow(icon, label, text) {
return `
<div style="display:flex;gap:var(--space-3);align-items:flex-start">
<div style="margin-top:1px">${icon}</div>
<div>
<div style="font-weight:700;font-size:var(--text-sm)">${label}</div>
<div style="color:var(--c-text-secondary);font-size:var(--text-sm)">${text}</div>
</div>
</div>`;
}
function _renderStatus(app) {
const statusMap = {
pending: { icon: 'clock', text: 'Deine Bewerbung ist eingegangen — wir melden uns bald!', color: 'var(--c-text-secondary)' },
reviewing: { icon: 'magnifying-glass', text: 'Wir schauen uns deine Bewerbung gerade genauer an.', color: '#0284c7' },
accepted: { icon: 'check-circle', text: 'Herzlichen Glückwunsch — du bist dabei!', color: 'var(--c-success)' },
rejected: { icon: 'x', text: 'Es hat diesmal leider nicht geklappt.', color: 'var(--c-danger)' },
};
const s = statusMap[app.status] || statusMap.pending;
return `
<div class="card" style="padding:var(--space-5);text-align:center">
<div style="margin-bottom:var(--space-3)">
<svg class="ph-icon" aria-hidden="true" style="width:48px;height:48px;color:${s.color}"><use href="/icons/phosphor.svg#${s.icon}"></use></svg>
</div>
<div style="font-weight:700;color:${s.color};font-size:var(--text-lg);margin-bottom:var(--space-2)">${s.text}</div>
<div style="color:var(--c-text-muted);font-size:var(--text-xs)">
Bewerbung eingereicht: ${app.created_at?.slice(0,10)}
</div>
${app.admin_note ? `<div style="margin-top:var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-md);padding:var(--space-3);font-size:var(--text-sm);
color:var(--c-text-secondary);text-align:left">${_esc(app.admin_note)}</div>` : ''}
</div>`;
}
function _renderForm() {
const u = _appState.user;
return `
<div class="card">
<div style="padding:var(--space-5)">
<h2 style="font-size:var(--text-lg);font-weight:700;margin:0 0 var(--space-4);color:var(--c-primary)">
Jetzt bewerben
</h2>
<form id="jobs-form" novalidate>
<div class="form-group">
<label class="form-label">Dein Name *</label>
<input class="form-control" type="text" name="name"
value="${u ? _esc(u.name) : ''}" placeholder="Vorname oder Nickname" required>
</div>
<div class="form-group">
<label class="form-label">E-Mail *</label>
<input class="form-control" type="email" name="email"
value="${u ? _esc(u.email || '') : ''}" placeholder="deine@email.de" required>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)">
<div class="form-group" style="margin:0">
<label class="form-label">Hunde-Name</label>
<input class="form-control" type="text" name="dog_name" placeholder="z. B. Bella">
</div>
<div class="form-group" style="margin:0">
<label class="form-label">Rasse</label>
<input class="form-control" type="text" name="dog_rasse" placeholder="z. B. Labrador">
</div>
</div>
<div class="form-group">
<label class="form-label">Instagram oder TikTok Handle *</label>
<div style="position:relative">
<span style="position:absolute;left:var(--space-3);top:50%;transform:translateY(-50%);
color:var(--c-text-muted)">@</span>
<input class="form-control" type="text" name="social_handle"
style="padding-left:var(--space-7)" placeholder="dein_handle" required>
</div>
<p style="margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--c-text-muted)">
Dein öffentliches Profil auf Instagram oder TikTok
</p>
</div>
<div class="form-group">
<label class="form-label">Warum du? *</label>
<textarea class="form-control" name="motivation" rows="5"
placeholder="Erzähl uns kurz wer du bist, was dich an Ban Yaro begeistert und was du dir von der Stelle vorstellst. Kein formeller Ton nötig — schreib einfach wie du sprichst." required
style="resize:vertical"></textarea>
<p style="margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--c-text-muted)">
Mindestens 80 Zeichen
</p>
</div>
<div class="form-group">
<label class="form-label">Anhänge (optional)</label>
<input class="form-control" type="file" name="files" id="jobs-files"
multiple accept=".pdf,.jpg,.jpeg,.png,.webp,.mp4,.mov"
style="padding:var(--space-2)">
<p style="margin:var(--space-1) 0 0;font-size:var(--text-xs);color:var(--c-text-muted)">
Beispiel-Posts, Portfolio, kurzes Video von dir und deinem Hund — max. 3 Dateien, je 10 MB.
PDF, Bild oder Video.
</p>
</div>
${!u ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-3);font-size:var(--text-sm);color:var(--c-text-secondary);
margin-bottom:var(--space-4)">
💡 <b>Tipp:</b> Wenn du dich vorher
<a href="#" id="jobs-login-link" style="color:var(--c-primary)">anmeldest oder registrierst</a>,
bekommst du sofort den 14-tägigen Luna-Probezugang.
</div>` : ''}
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2);display:flex;align-items:center;justify-content:center;gap:var(--space-2)">
<svg class="ph-icon" aria-hidden="true" style="width:18px;height:18px"><use href="/icons/phosphor.svg#sparkle"></use></svg>
Bewerbung absenden + Luna freischalten
</button>
</form>
</div>
</div>`;
}
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 };
})();