const resourceName = typeof GetParentResourceName === 'function' ? GetParentResourceName() : 'bleeter'; const state = { visible: false, mode: 'phone', // 'phone' | 'desktop' page: 'feed', data: null, fallback: true, likedPosts: new Set([201]), commentsOpen: new Set(), searchQuery: '', selectedCalendarDay: 3, marketQuery: '', marketSort: 'newest', likeDeltas: {}, postComments: {}, likedComments: new Set(), commentLikeDeltas: {}, editingProfile: false, pendingUploadTarget: null, composeMedia: {}, viewedProfileHandle: null }; const fallbackData = { account: { displayName: 'Zoey Reed', mailAddress: 'zoeyreed@mail.ls' }, navigation: [], profiles: [ { id: 1, profile_type: 'private', handle: 'zoeyreed', display_name: 'Zoey Reed', avatar_url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=256&h=256&fit=crop', banner_url: 'https://images.unsplash.com/photo-1519608487953-e999c86e7455?w=1200&h=320&fit=crop', bio: 'Los Santos, Kaffee, Geschichten und viel zu viele Benachrichtigungen.', is_verified: false }, { id: 2, profile_type: 'small_business', handle: 'reed.events', display_name: 'Reed Events', avatar_url: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=256&h=256&fit=crop', banner_url: 'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=1200&h=320&fit=crop', bio: 'Kleine Events, grosse Abende.', is_verified: true } ], activeProfileId: 1, profileDirectory: [], posts: [ { id: 101, feed_type: 'home', author: { handle: 'lasky', display_name: 'A.J. Lasky III', avatar_url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=256&h=256&fit=crop', is_verified: false }, body: '"Er ist ein stiller Waechter .... ein wachsamer Beschuetzer. Ein dunkler Ritter" - Jim Gordon', media_url: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1100&h=680&fit=crop', created_at: 'Heute, 22:15', likes: 12, comments: 3 }, { id: 102, feed_type: 'home', author: { handle: 'm4ttr1x', display_name: '@m4ttr1x', avatar_url: 'https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=256&h=256&fit=crop', is_verified: false }, body: 'Wir haben zugesehen. ShitInvaders Sicherheit? Laecherlich. Ihr System ist genauso kaputt wie ihre Versprechen.\n\nDas hier ist euer Weckruf.', created_at: '28.12.2024, 22:21:53', likes: 1, comments: 0 }, { id: 201, feed_type: 'advertising', author: { handle: 'lsmd', display_name: 'Los Santos Medical Department', avatar_url: 'https://images.unsplash.com/photo-1584515933487-779824d29309?w=256&h=256&fit=crop', is_verified: true }, body: 'Heute Blutspendeaktion am Pillbox Medical Center. Kommt vorbei, Los Santos.', created_at: 'Heute, 19:40', likes: 44, comments: 8 } ], businesses: [ { handle: 'dmv', display_name: 'Department of Motorvehicles', avatar_url: 'https://images.unsplash.com/photo-1511919884226-fd3cad34687c?w=256&h=256&fit=crop', banner_url: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=1000&h=260&fit=crop', status: 'open', is_verified: true }, { handle: 'doj', display_name: 'Department of Justice', avatar_url: 'https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=256&h=256&fit=crop', banner_url: 'https://images.unsplash.com/photo-1453945619913-79ec89a82c51?w=1000&h=260&fit=crop', status: 'open', is_verified: true }, { handle: 'gruppe6', display_name: 'Gruppe 6', avatar_url: 'https://images.unsplash.com/photo-1517048676732-d65bc937f952?w=256&h=256&fit=crop', banner_url: 'https://images.unsplash.com/photo-1521791136064-7986c2920216?w=1000&h=260&fit=crop', status: 'closed', is_verified: false }, { handle: 'herrkutz', display_name: 'Herr Kutz', avatar_url: 'https://images.unsplash.com/photo-1503951914875-452162b0f3f1?w=256&h=256&fit=crop', banner_url: 'https://images.unsplash.com/photo-1585747860715-2ba37e788b70?w=1000&h=260&fit=crop', status: 'closed', is_verified: true } ], suggestions: [ { handle: 'lsmd', display_name: 'Los Santos Medical Department' }, { handle: 'dynasty8', display_name: 'Dynasty 8' }, { handle: 'usms', display_name: 'US Marshal Service' } ], trends: [ { tag: '#gruppe6', posts: 77 }, { tag: '#lalintera', posts: 43 }, { tag: '#JoinTheTeam', posts: 39 }, { tag: '#LSFD', posts: 39 } ], users: [ { handle: 'zoeyreed', display_name: 'Zoey Reed', avatar_url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=256&h=256&fit=crop', profile_type: 'private', can_be_blocked: true }, { handle: 'zoey.reed', display_name: 'Zoey Reed', avatar_url: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=256&h=256&fit=crop', profile_type: 'private', can_be_blocked: true }, { handle: 'z_reed', display_name: 'Z. Reed', avatar_url: 'https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?w=256&h=256&fit=crop', profile_type: 'private', can_be_blocked: true }, { handle: 'lasky', display_name: 'A.J. Lasky III', avatar_url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=256&h=256&fit=crop', profile_type: 'private', can_be_blocked: true } ], social: { following: [ { handle: 'lasky', display_name: 'A.J. Lasky III', avatar_url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=256&h=256&fit=crop', profile_type: 'private', can_be_blocked: true } ], followers: [], blocked: [] }, marketplace: [ { id: 1, title: 'BMX in gutem Zustand', body: 'Schwarzes BMX, kaum gefahren. Abholung in Vespucci.', price: 450, price_label: '$450', created: 4, can_delete: true, author: { handle: 'zoeyreed', display_name: 'Zoey Reed', avatar_url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=256&h=256&fit=crop', email: 'zoeyreed@mail.ls', is_verified: false } }, { id: 2, title: 'Catering fuer kleine Feiern', body: 'Fingerfood und Getraenke fuer Events bis 25 Personen.', price: 1200, price_label: '$1200', created: 2, can_delete: false, author: { handle: 'reed.events', display_name: 'Reed Events', avatar_url: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=256&h=256&fit=crop', email: 'reed.events@mail.ls', is_verified: true } }, { id: 3, title: 'Reinigung nach Clubabend', body: 'Diskret, schnell, mit eigenem Equipment.', price: 800, price_label: '$800', created: 1, can_delete: false, author: { handle: 'cleanls', display_name: 'Clean LS', avatar_url: '', email: 'cleanls@mail.ls', is_verified: false } } ], calendarDays: [ { offset: 0, date: '2026-05-19', title: 'Dienstag 19.05.2026', events: [{ id: 1, time: '18:00', title: 'LSPD Coffe with Cops', location: '', author: 'CoolBeans', can_delete: false }] }, { offset: 1, date: '2026-05-20', title: 'Mittwoch 20.05.2026', events: [] }, { offset: 2, date: '2026-05-21', title: 'Donnerstag 21.05.2026', events: [] }, { offset: 3, date: '2026-05-22', title: 'Freitag 22.05.2026', events: [{ id: 2, time: '20:00', title: 'Happy Hour', location: 'Craftbar', author: 'Craftbar', can_delete: false }, { id: 3, time: '21:00', title: 'All you can eat', location: '', author: 'Burgershot', can_delete: false }] }, { offset: 4, date: '2026-05-23', title: 'Samstag 23.05.2026', events: [{ id: 4, time: '21:00', title: 'Geocaching Legionsquar', location: '', author: 'Lifeinvader', can_delete: false }] }, { offset: 5, date: '2026-05-24', title: 'Sonntag 24.05.2026', events: [] }, { offset: 6, date: '2026-05-25', title: 'Montag 25.05.2026', events: [] }, { offset: 7, date: '2026-05-26', title: 'Dienstag 26.05.2026', events: [] }, { offset: 8, date: '2026-05-27', title: 'Mittwoch 27.05.2026', events: [] }, { offset: 9, date: '2026-05-28', title: 'Donnerstag 28.05.2026', events: [] }, { offset: 10, date: '2026-05-29', title: 'Freitag 29.05.2026', events: [] }, { offset: 11, date: '2026-05-30', title: 'Samstag 30.05.2026', events: [] }, { offset: 12, date: '2026-05-31', title: 'Sonntag 31.05.2026', events: [] }, { offset: 13, date: '2026-06-01', title: 'Montag 01.06.2026', events: [] }, { offset: 14, date: '2026-06-02', title: 'Dienstag 02.06.2026', events: [] }, { offset: 15, date: '2026-06-03', title: 'Mittwoch 03.06.2026', events: [] }, { offset: 16, date: '2026-06-04', title: 'Donnerstag 04.06.2026', events: [] }, { offset: 17, date: '2026-06-05', title: 'Freitag 05.06.2026', events: [] }, { offset: 18, date: '2026-06-06', title: 'Samstag 06.06.2026', events: [] }, { offset: 19, date: '2026-06-07', title: 'Sonntag 07.06.2026', events: [] }, { offset: 20, date: '2026-06-08', title: 'Montag 08.06.2026', events: [] }, { offset: 21, date: '2026-06-09', title: 'Dienstag 09.06.2026', events: [] }, { offset: 22, date: '2026-06-10', title: 'Mittwoch 10.06.2026', events: [] }, { offset: 23, date: '2026-06-11', title: 'Donnerstag 11.06.2026', events: [] }, { offset: 24, date: '2026-06-12', title: 'Freitag 12.06.2026', events: [] }, { offset: 25, date: '2026-06-13', title: 'Samstag 13.06.2026', events: [] }, { offset: 26, date: '2026-06-14', title: 'Sonntag 14.06.2026', events: [] }, { offset: 27, date: '2026-06-15', title: 'Montag 15.06.2026', events: [] } ], config: { navigation: [ { key: 'feed', label: 'Feed', icon: 'H' }, { key: 'ads', label: 'Werbefeed', icon: 'W' }, { key: 'followers', label: 'Follower', icon: 'F' }, { key: 'market', label: 'Marktplatz', icon: 'M' }, { key: 'calendar', label: 'Kalender', icon: 'K' }, { key: 'business', label: 'Gewerbe', icon: 'G' }, { key: 'profile', label: 'Profil', icon: 'P' }, { key: 'legal', label: 'Rechtliches', icon: 'R' } ] } }; const root = document.getElementById('bleeter-root'); const nav = document.getElementById('side-nav'); const content = document.getElementById('content'); const context = document.getElementById('context'); const topProfileAvatar = document.getElementById('top-profile-avatar'); const globalSearch = document.getElementById('global-search'); const searchResults = document.getElementById('search-results'); const mediaFileInput = document.getElementById('media-file-input'); function post(name, payload = {}) { return fetch(`https://${resourceName}/${name}`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=UTF-8' }, body: JSON.stringify(payload) }).catch(() => {}); } function data() { return state.data || fallbackData; } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); } function activeProfile() { const current = data(); return (current.profiles || []).find(profile => profile.id === current.activeProfileId) || current.profiles?.[0] || {}; } function allKnownProfiles() { const current = data(); const byHandle = new Map(); const addProfile = item => { if (!item?.handle) return; byHandle.set(item.handle, { ...(byHandle.get(item.handle) || {}), ...item }); }; (current.profileDirectory || []).forEach(addProfile); (current.profiles || []).forEach(addProfile); (current.users || []).forEach(addProfile); (current.businesses || []).forEach(addProfile); (current.social?.following || []).forEach(addProfile); (current.social?.followers || []).forEach(addProfile); (current.social?.blocked || []).forEach(addProfile); (current.posts || []).forEach(postItem => addProfile(postItem.author)); (current.marketplace || []).forEach(item => addProfile(item.author)); return [...byHandle.values()]; } function findKnownProfile(handle) { const cleanHandle = String(handle || '').replace(/^@/, ''); if (!cleanHandle) return null; return allKnownProfiles().find(profile => profile.handle === cleanHandle) || null; } function viewedProfile() { if (!state.viewedProfileHandle) return activeProfile(); return findKnownProfile(state.viewedProfileHandle) || activeProfile(); } function isOwnProfile(profile) { const active = activeProfile(); return Number(profile?.id) === Number(active.id) || profile?.handle === active.handle; } function openProfile(handle) { const cleanHandle = String(handle || '').replace(/^@/, ''); state.viewedProfileHandle = cleanHandle || null; state.editingProfile = false; state.page = 'profile'; state.searchQuery = ''; render(); } function isServerBacked() { return state.data !== null; } function lifeinvaderPermissions() { const permissions = data().permissions?.lifeinvader; return typeof permissions === 'object' && permissions !== null ? permissions : {}; } function hydrateInteractionState() { state.likedPosts = new Set(); state.likedComments = new Set(); state.postComments = {}; state.likeDeltas = {}; state.commentLikeDeltas = {}; for (const postItem of state.data?.posts || []) { if (postItem.liked_by_viewer) { state.likedPosts.add(postItem.id); } if (Array.isArray(postItem.comments_list)) { state.postComments[postItem.id] = postItem.comments_list; for (const comment of postItem.comments_list) { if (comment.liked_by_viewer) { state.likedComments.add(String(comment.id)); } } } } } function verified(profile) { return profile?.is_verified ? `${patchCheckIcon()}` : ''; } function lifeinvaderStaffBadge(profile) { return profile?.is_lifeinvader_staff ? `${awardIcon()}` : ''; } function profileBadges(profile) { return `${verified(profile)}${lifeinvaderStaffBadge(profile)}`; } function patchCheckIcon() { return ` `; } function awardIcon() { return ` `; } function slashCircleIcon() { return ` `; } function renderLifeinvaderProfileActions(profile) { const permissions = lifeinvaderPermissions(); const buttons = []; const handle = escapeHtml(profile.handle || ''); if (permissions.can_verify) { buttons.push(` `); } if (permissions.can_staff) { buttons.push(` `); } if (permissions.can_lock) { buttons.push(` `); } return buttons.length ? `
${buttons.join('')}
` : ''; } function trashIcon() { return ` `; } function mailIcon() { return ` `; } function profileStatIcon(type) { const icons = { posts: ` `, followers: ` `, following: ` `, market: ` `, calendar: ` ` }; return icons[type] || ''; } function navIconGlyph(icon, label) { return { home: 'F', megaphone: 'W', users: 'Fo', basket: 'M', calendar: 'K', briefcase: 'G', user: 'P', book: 'R' }[icon] || String(label || '?').slice(0, 2); } function navIconMarkup(item) { const icons = { feed: ` `, ads: ` `, followers: ` `, market: ` `, calendar: ` `, business: ` `, profile: ` `, legal: ` ` }; return icons[item.key] || escapeHtml(navIconGlyph(item.icon, item.label)); } function show(open, mode) { state.visible = open; if (open && mode) state.mode = mode; root.classList.toggle('hidden', !open); root.classList.toggle('mode-desktop', open && state.mode === 'desktop'); root.classList.toggle('mode-phone', open && state.mode !== 'desktop'); if (open) { post('requestBootstrap'); } render(); } function renderNav() { const items = data().config?.navigation || fallbackData.config.navigation; nav.innerHTML = items.map(item => ` `).join(''); } function renderTopbar() { const profile = activeProfile(); topProfileAvatar.src = profile.avatar_url || ''; if (globalSearch.value !== state.searchQuery) { globalSearch.value = state.searchQuery; } renderSearchResults(); } function renderSearchResults() { const query = state.searchQuery.trim().toLowerCase().replace(/^@/, ''); if (!query) { searchResults.classList.add('hidden'); searchResults.innerHTML = ''; return; } const matches = (data().users || fallbackData.users).filter(user => user.handle.startsWith(query)).slice(0, 5); searchResults.classList.toggle('hidden', matches.length === 0); searchResults.innerHTML = matches.map(user => ` `).join(''); } function renderCompose(feedType) { const profile = activeProfile(); const canPost = (profile.profile_type === 'private' && feedType === 'home') || (['small_business', 'company'].includes(profile.profile_type) && feedType === 'advertising') || (profile.profile_type === 'authority'); if (!canPost) { return `

Nur lesen

Dein aktives Profil darf hier liken und kommentieren, aber nicht posten.

`; } return `
JPG/PNG, max. 2 MB
`; } function renderPost(post) { const author = post.author || {}; const liked = state.likedPosts.has(post.id); const commentsOpen = state.commentsOpen.has(post.id); const likeCount = Number(post.likes || 0) + Number(state.likeDeltas[post.id] || 0); const comments = commentsForPost(post.id); const commentCount = comments.length; return `
@${escapeHtml(author.handle)}
${escapeHtml(post.created_at)}
${post.body ? `
${escapeHtml(post.body)}
` : ''} ${post.media_url ? `` : ''} ${commentsOpen ? renderComments(post, comments) : ''}
`; } function commentsForPost(postId) { if (!state.postComments[postId]) { const postItem = (data().posts || []).find(item => Number(item.id) === Number(postId)); state.postComments[postId] = Array.isArray(postItem?.comments_list) ? postItem.comments_list : [ { id: `${postId}-1`, author: 'zoeyreed', body: 'Guter Hinweis, danke fuer die Info.', likes: 0 }, { id: `${postId}-2`, author: 'lasky', body: 'Bin dabei.', likes: 0 } ]; } return state.postComments[postId]; } function renderComments(post, comments) { return `
${comments.map(comment => renderComment(comment)).join('')}
`; } function renderComment(comment) { const commentId = String(comment.id); const liked = state.likedComments.has(commentId); const likeCount = Number(comment.likes || 0) + Number(state.commentLikeDeltas[comment.id] || 0); return `
${escapeHtml(comment.body)}
${comment.can_delete ? `` : ''}
`; } function renderFeed(feedType) { const title = feedType === 'home' ? 'Feed' : 'Werbefeed'; const posts = (data().posts || []).filter(post => post.feed_type === feedType); content.innerHTML = `

${title}

${renderCompose(feedType)}
${posts.map(renderPost).join('')}
`; } function renderContextFeed() { const current = data(); context.innerHTML = `

Wem folgen?

${(current.suggestions || []).map(item => `
${escapeHtml(item.display_name.slice(0, 1))}

${escapeHtml(item.display_name)}

@${escapeHtml(item.handle)}

`).join('')}

Trends

${(current.trends || []).map(item => `

${escapeHtml(item.tag)}

${Number(item.posts || 0)} Posts

`).join('')}
`; } function renderBusiness() { const current = data(); const open = (current.businesses || []).filter(item => item.status === 'open'); const closed = (current.businesses || []).filter(item => item.status !== 'open'); const card = item => ` `; content.innerHTML = `

Gewerbe

${open.map(card).join('')}
${closed.map(card).join('')}
`; context.innerHTML = `

Status

${open.length} geoeffnet

Aktive Leitstellen werden hervorgehoben.

${closed.length} geschlossen

Geschlossene Gewerbe sind ausgegraut.

`; } function renderMarketplace() { const items = filteredMarketplaceItems(); content.innerHTML = `

Marktplatz

${renderMarketplaceCompose()}
${items.map(renderMarketplaceItem).join('')}
`; context.innerHTML = `

Sortierung

${items.length} Treffer

Suche nach Begriffen im Titel, Text oder Profilnamen.

`; } function filteredMarketplaceItems() { const query = state.marketQuery.trim().toLowerCase(); let items = (data().marketplace || fallbackData.marketplace).filter(item => { if (!query) return true; return `${item.title} ${item.body} ${item.author?.handle || ''} ${item.author?.display_name || ''}`.toLowerCase().includes(query); }); return [...items].sort((a, b) => { if (state.marketSort === 'price_asc') return Number(a.price || 0) - Number(b.price || 0); if (state.marketSort === 'price_desc') return Number(b.price || 0) - Number(a.price || 0); if (state.marketSort === 'oldest') return Number(a.created || 0) - Number(b.created || 0); return Number(b.created || 0) - Number(a.created || 0); }); } function refreshMarketplaceResults() { const items = filteredMarketplaceItems(); const results = document.getElementById('market-results'); if (results) { results.innerHTML = items.map(renderMarketplaceItem).join(''); } const hitCount = document.getElementById('market-hit-count'); if (hitCount) { hitCount.textContent = `${items.length} Treffer`; } } function renderMarketplaceItem(item) { const author = item.author || {}; const mailSubject = `Bleeter Marktplatz: ${item.title || 'Inserat'}`; return `
${author.email ? `` : ''}

@${escapeHtml(author.handle || '')}

${escapeHtml(item.title)}

${escapeHtml(item.body)}

${item.media_url ? `` : ''}
${escapeHtml(item.price_label || ('$' + Number(item.price || 0)))}
${item.can_delete ? `` : ''}
`; } function renderProfileEventItem(day, event) { const location = event.location ? ` ..${escapeHtml(event.location)}..` : ''; return `

${escapeHtml(day.title)}

${escapeHtml(event.time || '')} ${escapeHtml(event.title || '')}${location}

@${escapeHtml(event.author || '')}

`; } function renderMarketplaceCompose() { const profile = activeProfile(); return `
JPG/PNG, max. 2 MB
`; } function renderCalendar() { const days = data().calendarDays || fallbackData.calendarDays; const selected = days.find(day => day.offset === state.selectedCalendarDay) || days[0]; const profile = activeProfile(); const canCreateEvent = ['small_business', 'company', 'authority'].includes(profile.profile_type); content.innerHTML = `

Kalender

${days.map(day => `
${escapeHtml(day.title)}
${day.events.map(event => renderCalendarEventLine(event)).join('')}
`).join('')}
`; context.innerHTML = `

Veranstaltung

${canCreateEvent ? renderEventEditor(selected, profile) : `

Nur lesen

Termine koennen von Unternehmen, Kleingewerben und Behoerden eingetragen werden.

`} `; } function renderCalendarEventLine(event) { if (typeof event === 'string') { return `
${escapeHtml(event)}
`; } const location = event.location ? ` ..${escapeHtml(event.location)}..` : ''; const line = `${escapeHtml(event.time || '')} ${escapeHtml(event.title || '')} ${location} @${escapeHtml(event.author || '')}`; return `
${line} ${event.can_delete ? `` : ''}
`; } function renderEventEditor(selected, profile) { return `
${escapeHtml(selected.title)} @${escapeHtml(profile.handle || 'profil')}
`; } function renderLegal() { content.innerHTML = `

Rechtliches

`; context.innerHTML = ''; } function renderProfile() { const profile = viewedProfile(); const ownProfile = isOwnProfile(profile); const social = data().social || fallbackData.social; const following = socialHandles(social.following).has(profile.handle); const blocked = socialHandles(social.blocked).has(profile.handle); const visiblePosts = (data().posts || []).filter(post => post.author?.handle === profile.handle); const visibleMarketplace = (data().marketplace || []).filter(item => item.author?.handle === profile.handle); const visibleEvents = (data().calendarDays || []) .flatMap(day => (day.events || []) .filter(event => event.author === profile.handle) .map(event => ({ day, event }))); const followersFallback = (social.followers || []).filter(item => item.handle === profile.handle).length; const followingFallback = ownProfile ? (social.following || []).length : 0; const followersCount = Number(profile.followers_count ?? followersFallback); const followingCount = Number(profile.following_count ?? followingFallback); const canBlock = profile.can_be_blocked !== false; const avatarUrl = profile.avatar_url || ''; const bannerUrl = profile.banner_url || profile.avatar_url || ''; const contactMail = profile.email_contact || profile.email || ''; const moderationActions = renderLifeinvaderProfileActions(profile); if (ownProfile && state.editingProfile && profile.can_edit_profile !== false) { content.innerHTML = renderProfileEditor(profile); } else { const profileActions = ownProfile ? `
${profile.can_edit_profile !== false ? `` : ''} ${moderationActions}
` : `
${following ? `` : ``} ${contactMail ? `` : ''} ${blocked ? `` : (canBlock ? `` : `Geschuetzt`)} ${moderationActions}
`; content.innerHTML = `
${bannerUrl ? `` : '
'}
${avatarUrl ? `` : `
${escapeHtml((profile.display_name || profile.handle || '?').slice(0, 1))}
`}

${escapeHtml(profile.display_name || profile.handle || 'Profil')} ${profileBadges(profile)}

@${escapeHtml(profile.handle)}

${escapeHtml(profile.bio || '')}

${profileActions}

Profilinhalte

${ownProfile ? 'Deine sichtbaren Inhalte.' : `Sichtbare Inhalte von @${escapeHtml(profile.handle)}.`}

Posts

${visiblePosts.length ? visiblePosts.map(renderPost).join('') : '

Noch keine sichtbaren Posts.

'}
${visibleMarketplace.length ? `

Marktplatz

${visibleMarketplace.map(renderMarketplaceItem).join('')}
` : ''} ${visibleEvents.length ? `

Kalender

${visibleEvents.map(item => renderProfileEventItem(item.day, item.event)).join('')}
` : ''} `; } if (ownProfile) { context.innerHTML = `

Profil wechseln

${(data().profiles || []).map(item => ` `).join('')}
`; } else { context.innerHTML = `

Profil

`; } } function renderProfileEditor(profile) { const bioMax = profile.profile_type === 'private' ? 200 : 500; return `

Profil bearbeiten

@${escapeHtml(profile.handle)}

`; } function renderSimplePage(title, subtitle, cards, contextTitle = '') { content.innerHTML = `

${escapeHtml(title)}

${escapeHtml(subtitle)}

${cards.map(card => `

${escapeHtml(card.title)}

${escapeHtml(card.text)}

`).join('')}
`; context.innerHTML = contextTitle ? `

${escapeHtml(contextTitle)}

V1

Diese Ansicht ist im Grundgeruest vorbereitet und bekommt als naechstes echte Daten.

` : ''; } function socialHandles(list) { return new Set((list || []).map(item => item.handle)); } function renderSocialRow(item, mode, followingSet, blockedSet) { const handle = item.handle || ''; const isFollowing = followingSet.has(handle); const isBlocked = blockedSet.has(handle); const canBlock = item.can_be_blocked !== false; const blockAction = isBlocked ? `` : (canBlock ? `` : `Geschuetzt`); const followAction = mode === 'blocked' ? '' : (isFollowing ? `` : ``); return `
${item.avatar_url ? `` : `${escapeHtml((item.display_name || handle || '?').slice(0, 1))}`}
${escapeHtml(item.display_name || handle)} ${profileBadges(item)}

@${escapeHtml(handle)}

${followAction}${blockAction}
`; } function renderSocialSection(title, items, mode, followingSet, blockedSet, emptyText) { const rows = (items || []).map(item => renderSocialRow(item, mode, followingSet, blockedSet)).join(''); return `

${escapeHtml(title)}

${rows || `

${escapeHtml(emptyText)}

`}
`; } function renderFollowers() { const current = data(); const social = current.social || fallbackData.social; const followingSet = socialHandles(social.following); const blockedSet = socialHandles(social.blocked); const suggestions = (current.users || fallbackData.users).filter(item => !blockedSet.has(item.handle)).slice(0, 12); content.innerHTML = `

Follower

${renderSocialSection('Entdecken', suggestions, 'discover', followingSet, blockedSet, 'Keine Profile gefunden.')} ${renderSocialSection('Wem folge ich', social.following, 'following', followingSet, blockedSet, 'Du folgst aktuell niemandem.')} ${renderSocialSection('Wer folgt mir', social.followers, 'followers', followingSet, blockedSet, 'Noch keine Follower.')} ${renderSocialSection('Blockiert', social.blocked, 'blocked', followingSet, blockedSet, 'Keine blockierten Profile.')} `; context.innerHTML = ''; } function renderPage() { if (state.page === 'feed') { renderFeed('home'); renderContextFeed(); return; } if (state.page === 'ads') { renderFeed('advertising'); renderContextFeed(); return; } if (state.page === 'business') { renderBusiness(); return; } if (state.page === 'profile') { renderProfile(); return; } if (state.page === 'followers') { renderFollowers(); return; } if (state.page === 'market') { renderMarketplace(); return; } if (state.page === 'calendar') { renderCalendar(); return; } if (state.page === 'legal') { renderLegal(); } } function render() { renderNav(); renderTopbar(); renderPage(); } document.addEventListener('click', event => { const pageButton = event.target.closest('[data-page]'); if (pageButton) { state.page = pageButton.dataset.page; if (state.page === 'profile') state.viewedProfileHandle = null; if (state.page !== 'profile') state.editingProfile = false; render(); return; } const openOwnProfileButton = event.target.closest('[data-open-own-profile]'); if (openOwnProfileButton) { state.viewedProfileHandle = null; state.page = 'profile'; state.editingProfile = false; render(); return; } const openProfileButton = event.target.closest('[data-open-profile]'); if (openProfileButton) { openProfile(openProfileButton.dataset.openProfile); return; } const mailButton = event.target.closest('[data-mail-to]'); if (mailButton) { post('openMailTo', { mail: mailButton.dataset.mailTo, subject: mailButton.dataset.mailSubject || '' }); return; } const moderateProfileButton = event.target.closest('[data-moderate-profile]'); if (moderateProfileButton) { const action = moderateProfileButton.dataset.moderateProfile; const handle = moderateProfileButton.dataset.moderateProfileHandle; if (!isServerBacked()) { const profile = findKnownProfile(handle); if (profile) { if (action === 'verify') profile.is_verified = !profile.is_verified; if (action === 'staff') profile.is_lifeinvader_staff = !profile.is_lifeinvader_staff; if (action === 'lock') profile.is_locked = !profile.is_locked; } } post('moderateProfile', { action, handle }); render(); return; } const profileJumpButton = event.target.closest('[data-profile-jump]'); if (profileJumpButton) { const target = document.querySelector(`[data-profile-section="${profileJumpButton.dataset.profileJump}"]`); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } return; } const profileSocialButton = event.target.closest('[data-profile-social]'); if (profileSocialButton) { state.page = 'followers'; state.editingProfile = false; render(); return; } const followButton = event.target.closest('[data-follow-profile]'); if (followButton) { const handle = followButton.dataset.followProfile; if (!isServerBacked()) { const item = fallbackData.users.find(user => user.handle === handle); if (item && !fallbackData.social.following.some(user => user.handle === handle)) { fallbackData.social.following.push(item); } } post('followProfile', { handle }); render(); return; } const unfollowButton = event.target.closest('[data-unfollow-profile]'); if (unfollowButton) { const handle = unfollowButton.dataset.unfollowProfile; if (!isServerBacked()) { fallbackData.social.following = fallbackData.social.following.filter(user => user.handle !== handle); } post('unfollowProfile', { handle }); render(); return; } const blockButton = event.target.closest('[data-block-profile]'); if (blockButton) { const handle = blockButton.dataset.blockProfile; if (!isServerBacked()) { const item = fallbackData.users.find(user => user.handle === handle) || fallbackData.social.following.find(user => user.handle === handle); if (item?.can_be_blocked !== false && !fallbackData.social.blocked.some(user => user.handle === handle)) { fallbackData.social.blocked.push(item); fallbackData.social.following = fallbackData.social.following.filter(user => user.handle !== handle); fallbackData.social.followers = fallbackData.social.followers.filter(user => user.handle !== handle); } } post('blockProfile', { handle }); render(); return; } const unblockButton = event.target.closest('[data-unblock-profile]'); if (unblockButton) { const handle = unblockButton.dataset.unblockProfile; if (!isServerBacked()) { fallbackData.social.blocked = fallbackData.social.blocked.filter(user => user.handle !== handle); } post('unblockProfile', { handle }); render(); return; } const createPostButton = event.target.closest('[data-create-post]'); if (createPostButton) { const feedType = createPostButton.dataset.createPost; const form = createPostButton.closest('.compose'); const textarea = form?.querySelector('textarea'); const body = textarea?.value.trim(); const mediaInput = form?.querySelector('[data-compose-media]'); const mediaUrl = mediaInput?.value.trim() || state.composeMedia[feedType] || ''; if (!body && !mediaUrl) return; if (!isServerBacked()) { const profile = activeProfile(); fallbackData.posts.unshift({ id: Date.now(), feed_type: feedType, author: { id: profile.id, handle: profile.handle, display_name: profile.display_name, avatar_url: profile.avatar_url, is_verified: profile.is_verified }, body, media_url: mediaUrl, created_at: 'Gerade eben', likes: 0, comments: 0, comments_list: [], can_delete: true }); } textarea.value = ''; if (mediaInput) mediaInput.value = ''; state.composeMedia[feedType] = ''; post('createPost', { feedType, body, mediaUrl }); render(); return; } const profileButton = event.target.closest('[data-profile]'); if (profileButton) { const profileId = Number(profileButton.dataset.profile); if (state.data) state.data.activeProfileId = profileId; fallbackData.activeProfileId = profileId; state.viewedProfileHandle = null; state.editingProfile = false; post('setActiveProfile', { profileId }); render(); return; } if (event.target.closest('[data-edit-profile]')) { state.editingProfile = true; render(); return; } // Upload-Buttons sind jetzt