PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
269 lines
13 KiB
JavaScript
269 lines
13 KiB
JavaScript
/* ============================================================
|
||
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 & TikTok
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Stellenbeschreibung -->
|
||
<div class="card mb-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', '1–2 Posts pro Woche auf Instagram & 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 mb-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 class="mb-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"
|
||
class="p-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" class="text-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 };
|
||
})();
|