'use strict';
// ── State ─────────────────────────────────────────────────────────────────
const S = {
token: localStorage.getItem('bleeter_token') || null,
account: null,
profiles: [],
activeId: Number(localStorage.getItem('bleeter_profile')) || null,
canModerate: false,
view: 'home',
openComments: new Set(),
};
const PROFILE_TYPE_LABEL = {
private: 'Privat', small_business: 'Kleingewerbe', company: 'Unternehmen',
authority: 'Behörde', lifeinvader: 'Lifeinvader',
};
const BUSINESS_TYPES = ['small_business', 'company', 'authority'];
function activeProfile() { return S.profiles.find(p => p.id === S.activeId) || S.profiles[0] || null; }
function canPostHere(profile, feedType) {
if (!profile) return false;
const rights = { private: { home: 1 }, small_business: { advertising: 1 }, company: { advertising: 1 }, authority: { home: 1, advertising: 1 }, lifeinvader: {} };
return !!(rights[profile.profile_type] || {})[feedType];
}
// ── API ───────────────────────────────────────────────────────────────────
async function api(method, path, body, isForm) {
const headers = {};
if (S.token) headers['Authorization'] = 'Bearer ' + S.token;
let payload;
if (isForm) { payload = body; }
else if (body !== undefined) { headers['Content-Type'] = 'application/json'; payload = JSON.stringify(body); }
const res = await fetch(path, { method, headers, body: payload });
if (res.status === 401) { logout(); throw new Error('unauthorized'); }
let data = null; try { data = await res.json(); } catch (e) {}
if (!res.ok) { const e = new Error((data && data.error) || 'error'); e.data = data; throw e; }
return data;
}
function withProfile(params) { const p = activeProfile(); return Object.assign({ profileId: p ? p.id : '' }, params || {}); }
function qs(obj) { return '?' + new URLSearchParams(obj).toString(); }
function apiGet(path, params) { return api('GET', path + qs(withProfile(params))); }
function apiSend(method, path, body) { return api(method, path, withProfile(body)); }
// ── Helpers ─────────────────────────────────────────────────────────────────
function h(html) { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstElementChild; }
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
function initials(name) { return String(name || '?').trim().split(/\s+/).slice(0, 2).map(w => w[0] || '').join('').toUpperCase() || '?'; }
function avatar(url, name, size) {
size = size || 'md';
if (url) return ``;
return `
${esc(initials(name))}
`;
}
function badge(p) {
let out = '';
if (p.is_verified) out += ' ✔';
if (p.is_lifeinvader_staff) out += ' Staff';
return out;
}
function timeAgo(str) {
if (!str) return '';
const t = new Date(str.replace(' ', 'T')).getTime();
if (isNaN(t)) return str;
const s = Math.floor((Date.now() - t) / 1000);
if (s < 60) return 'gerade eben';
if (s < 3600) return Math.floor(s / 60) + ' Min.';
if (s < 86400) return Math.floor(s / 3600) + ' Std.';
if (s < 604800) return Math.floor(s / 86400) + ' T.';
return new Date(t).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' });
}
function toast(msg, type) {
const t = h(`
${esc(msg)}
`);
document.getElementById('toast-layer').appendChild(t);
setTimeout(() => { t.style.opacity = '0'; t.style.transition = '0.3s'; setTimeout(() => t.remove(), 300); }, 3200);
}
const ERR_DE = {
invalid_login: 'Handle/Mail oder Passwort falsch.', empty_post: 'Dein Post ist leer.',
not_allowed_here: 'Dieses Profil darf hier nicht posten.', media_rejected: 'Bild-URL abgelehnt.',
handle_taken: 'Dieses Handle ist bereits vergeben.', reserved: 'Dieses Handle ist reserviert.',
already_registered: 'Es existiert bereits ein Privatprofil.', name_required: 'Name darf nicht leer sein.',
cannot_block: 'Dieses Profil kann nicht blockiert werden.', no_lifeinvader_rights: 'Keine Lifeinvader-Rechte.',
invalid_date: 'Ungültiges Datum.', invalid_time: 'Ungültige Uhrzeit.', title_required: 'Titel fehlt.',
not_owner: 'Nur eigene Einträge können bearbeitet werden.', upload_failed: 'Upload fehlgeschlagen.',
};
function errText(e) { return ERR_DE[e && e.message] || 'Es ist ein Fehler aufgetreten.'; }
function modal(title, bodyEl, onSubmit, submitLabel) {
const layer = document.getElementById('modal-layer');
const m = h(`
`; }
async function viewFeed(feedType) {
const { posts } = await apiGet('/api/feed', { type: feedType });
const p = activeProfile();
const canPost = canPostHere(p, feedType);
const c = h(`
${topbar(feedType === 'advertising' ? 'Werbefeed' : 'Feed', feedType === 'advertising' ? 'Angebote & Ankündigungen von Gewerben und Behörden' : 'Was in Los Santos gerade passiert')}
`);
if (canPost) c.appendChild(composer(feedType));
else if (p) c.appendChild(h(`
Als ${esc(PROFILE_TYPE_LABEL[p.profile_type])}-Profil kannst du hier nicht posten. ${feedType === 'home' ? 'Wechsle auf ein Behördenprofil.' : 'Wechsle auf ein Gewerbe- oder Behördenprofil.'}
`));
if (!posts.length) c.appendChild(h(`
🐦
Noch keine Bleets hier.
`));
posts.forEach(post => c.appendChild(postCard(post)));
main().innerHTML = ''; main().appendChild(c);
}
function composer(feedType) {
const p = activeProfile();
const el = h(`
${avatar(p.avatar_url, p.display_name, 'md')}
0 / 2000
`);
const ta = el.querySelector('textarea'), cc = el.querySelector('.char-count');
ta.addEventListener('input', () => cc.textContent = `${ta.value.length} / 2000`);
el.querySelector('[data-post]').addEventListener('click', async (ev) => {
const body = ta.value.trim(), mediaUrl = el.querySelector('.media-url').value.trim();
if (!body && !mediaUrl) return toast('Dein Post ist leer.', 'error');
ev.target.disabled = true;
try { await apiSend('POST', '/api/posts', { feedType, body, mediaUrl }); toast('Gepostet.', 'success'); go(S.view); }
catch (e) { toast(errText(e), 'error'); ev.target.disabled = false; }
});
return el;
}
function postCard(post) {
const a = post.author;
const el = h(`
`);
if (p.can_edit_profile) c.querySelector('#edit-profile').addEventListener('click', () => editProfileModal(prof));
if (!data.posts.length) c.appendChild(h(`
Bleeter ist das soziale Netzwerk von Lifeinvader in Los Santos. Alle Inhalte sind fiktiv und Teil des Rollenspiels auf NaturalBornPlayers.
Nutzungsregeln
Es gelten die Serverregeln von NaturalBornPlayers. Beleidigungen, reale Werbung, OOC-Inhalte sowie das Teilen realer personenbezogener Daten sind untersagt.
Behörden- und Lifeinvader-Profile können nicht blockiert werden. Verstöße können von Lifeinvader-Mitarbeitern moderiert (Verifizierung, Sperrung) werden.
Inhalte & Haftung
Für Inhalte ist der jeweilige Verfasser (IC) verantwortlich. Bilder werden über einen externen Dienst gehostet.
Impressum (IC)
Lifeinvader Bleeter · Los Santos · vertreten durch die Lifeinvader-Redaktion.