/* global React, MZ, L */ const { useState, useEffect, useRef } = React; const ROLE_PL = { admin: 'administrator', moderator: 'moderator', trusted: 'zaufany', member: 'użytkownik' }; function Account({ userId, handle, setRoute, profile: myProfile, onToast }) { const [u, setU] = useState(null); const [videos, setVideos] = useState([]); const [tab, setTab] = useState('videos'); const [loading, setLoading] = useState(true); const isMe = myProfile?.id === (u?.id || userId); useEffect(() => { (async () => { setLoading(true); try { let prof; if (userId) prof = await MZ.getProfile(userId); else if (handle) { const { data } = await MZ.sb.from('profiles').select('*').eq('handle', handle).single(); prof = data; } if (!prof) throw new Error('not found'); const vids = await MZ.listUserVideos(prof.id); setU(prof); setVideos(vids); if (window.MZ_SEO) window.MZ_SEO.forAccount(prof.handle); } catch (e) { onToast('Nie znaleziono użytkownika'); } setLoading(false); })(); }, [userId, handle]); if (loading || !u) return

Ładowanie profilu…

; const publishedVideos = videos.filter(v => v.status === 'published'); const totalVotes = publishedVideos.reduce((s, v) => s + (v.up||0) - (v.down||0), 0); const totalViews = publishedVideos.reduce((s, v) => s + (v.views||0), 0); return (
{ e.preventDefault(); setRoute({name:'home'}); }}>START / PROFIL / @{u.handle.toUpperCase()}
{isMe && } {isMe && }
{tab === 'videos' && ( publishedVideos.length > 0 ? (
{publishedVideos.map(v => setRoute({name:'video', id})} />)}
) :

Brak opublikowanych nagrań.

)} {tab === 'pending' && isMe && (
{videos.filter(v=>v.status!=='published').length === 0 ?

Brak oczekujących nagrań.

: videos.filter(v=>v.status!=='published').map(v => (
{v.title}
{v.city || '—'} · {timeAgo(v.created_at)}
{v.status==='pending'?'moderacja':v.status==='rejected'?'odrzucone':'usunięte'}
)) }
)} {tab === 'settings' && isMe && }
); } function AccountSettings({ profile, setU, onToast }) { const [name, setName] = useState(profile.name || ''); const [busy, setBusy] = useState(false); const save = async (e) => { e.preventDefault(); setBusy(true); try { await MZ.updateProfile({ name }); setU({ ...profile, name }); onToast('Profil zapisany'); } catch (er) { onToast(er.message); } setBusy(false); }; const logoutAll = async () => { if (!confirm('Wylogować ze wszystkich urządzeń?')) return; await MZ.signOut(); location.reload(); }; return (
setName(e.target.value)} />
NAZWY NIE MOŻNA ZMIENIĆ
Twoje dane

Zgodnie z RODO masz prawo do dostępu, sprostowania, usunięcia i przeniesienia swoich danych. Skontaktuj się z nami: kontakt@miejscezdarzenia.pl

); } function Admin({ setRoute, onToast }) { const [section, setSection] = useState('overview'); const [counts, setCounts] = useState({ videos: 0, queue: 0, reports: 0, users: 0 }); useEffect(() => { MZ.stats().then(setCounts); }, [section]); const sections = [ { id:'overview', label:'Przegląd' }, { id:'queue', label:'Kolejka moderacji', count: counts.queue }, { id:'videos', label:'Wszystkie nagrania', count: counts.videos }, { id:'reports', label:'Zgłoszenia', count: counts.reports }, { id:'users', label:'Użytkownicy', count: counts.users }, ]; return (
{section === 'overview' && } {section === 'queue' && } {section === 'videos' && } {section === 'reports' && } {section === 'users' && }
); } function AdminOverview({ counts }) { return (
Admin

Przegląd

Opublikowane
{counts.videos}
W kolejce
{counts.queue}
Otwarte zgłoszenia
{counts.reports}
Użytkownicy
{counts.users}
); } function AdminQueue({ onToast }) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const load = async () => { setLoading(true); setItems(await MZ.adminListQueue()); setLoading(false); }; useEffect(() => { load(); }, []); const act = async (id, status) => { try { await MZ.adminSetStatus(id, status); onToast(status==='published'?'Zatwierdzone':'Odrzucone'); load(); } catch(e){ onToast(e.message); } }; return (
Moderacja

Kolejka ({items.length})

{loading ? : items.length === 0 ? : items.map(q => ( )) }
TytułAutorMiastoDodanoYTAkcje
Ładowanie…
Kolejka pusta.
{q.title} @{q.author_profile?.handle || '—'} {q.city || '—'} {fmtDate(q.created_at)} {q.youtube_id}
); } function AdminVideos({ setRoute, onToast }) { const [items, setItems] = useState([]); const [sev, setSev] = useState('all'); const [editing, setEditing] = useState(null); const [statusFilter, setStatusFilter] = useState('published'); const load = async () => setItems(await MZ.listVideos({ status: statusFilter, limit:500 })); useEffect(() => { load(); }, [statusFilter]); const list = sev === 'all' ? items : items.filter(v => v.severity === sev); const remove = async (id) => { if (!confirm('Usunąć nagranie?')) return; try { await MZ.adminSetStatus(id, 'removed'); onToast('Usunięte'); load(); } catch(e){ onToast(e.message); } }; const restore = async (id) => { try { await MZ.adminSetStatus(id, 'published'); onToast('Przywrócone'); load(); } catch(e){ onToast(e.message); } }; return (
Treści

Wszystkie nagrania ({items.length})

{['published','pending','rejected','removed'].map(s => ( ))}
{['all','collision','near-miss','wildlife','weather','other'].map(s => ( ))}
{list.map(v => ( ))}
TytułAutorLokalizacjaRodzajGłosyWyśw.Akcje
{ e.preventDefault(); setRoute({name:'video', id:v.id}); }}>{v.title} @{v.author_profile?.handle || '—'} {v.city || '—'} {SEV_PL[v.severity] || v.severity} +{fmt((v.up||0) - (v.down||0))} {fmt(v.views)}
{statusFilter === 'published' && } {statusFilter !== 'published' && }
{editing && setEditing(null)} onSaved={() => { setEditing(null); load(); onToast('Zapisane'); }} onToast={onToast} />}
); } function EditMapPicker({ lat, lng, onChange }) { const ref = useRef(null); const mapRef = useRef(null); const markerRef = useRef(null); useEffect(() => { if (!ref.current || mapRef.current) return; const startLat = parseFloat(lat) || 52.07; const startLng = parseFloat(lng) || 19.48; const map = L.map(ref.current, { zoomControl: true }).setView([startLat, startLng], lat ? 13 : 6); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap, © CARTO', maxZoom: 19, }).addTo(map); const marker = L.marker([startLat, startLng], { draggable: true }).addTo(map); marker.on('dragend', () => { const p = marker.getLatLng(); onChange(p.lat.toFixed(6), p.lng.toFixed(6)); }); map.on('click', (e) => { marker.setLatLng(e.latlng); onChange(e.latlng.lat.toFixed(6), e.latlng.lng.toFixed(6)); }); mapRef.current = map; markerRef.current = marker; setTimeout(() => map.invalidateSize(), 100); return () => { map.remove(); mapRef.current = null; markerRef.current = null; }; }, []); // sync external coord changes (typed into number inputs) → marker useEffect(() => { if (!markerRef.current) return; const la = parseFloat(lat), ln = parseFloat(lng); if (!isNaN(la) && !isNaN(ln)) { const cur = markerRef.current.getLatLng(); if (Math.abs(cur.lat - la) > 1e-5 || Math.abs(cur.lng - ln) > 1e-5) { markerRef.current.setLatLng([la, ln]); } } }, [lat, lng]); return
; } function EditVideoModal({ video, onClose, onSaved, onToast }) { const [d, setD] = useState({ title: video.title || '', severity: video.severity || 'other', city: video.city || '', description: video.description || '', tags: (video.tags || []).join(', '), duration: video.duration || '', start_at: video.start_at != null ? String(video.start_at) : '', end_at: video.end_at != null ? String(video.end_at) : '', youtube_id: video.youtube_id || '', lat: video.lat, lng: video.lng, recorded_on: video.recorded_on || '', status: video.status || 'published', }); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const set = (k, v) => setD(o => ({ ...o, [k]: v })); const save = async (e) => { e.preventDefault(); setBusy(true); setErr(null); try { const payload = { title: d.title.trim(), severity: d.severity, city: d.city.trim() || null, description: d.description.trim() || null, tags: d.tags.split(',').map(s => s.trim()).filter(Boolean), duration: d.duration.trim() || null, start_at: MZ.parseTime(d.start_at), end_at: MZ.parseTime(d.end_at), youtube_id: d.youtube_id.trim(), lat: parseFloat(d.lat), lng: parseFloat(d.lng), recorded_on: d.recorded_on || null, status: d.status, }; await MZ.adminUpdateVideo(video.id, payload); onSaved?.(); } catch (er) { setErr(er.message || 'Błąd zapisu'); } setBusy(false); }; // Track where mousedown started so dragging a text-selection over the // backdrop does NOT close the modal (only intentional clicks do). const downOnBackdrop = useRef(false); return (
{ downOnBackdrop.current = (e.target === e.currentTarget); }} onClick={e => { if (e.target === e.currentTarget && downOnBackdrop.current) onClose(); }} >
e.stopPropagation()} onClick={e => e.stopPropagation()} onSubmit={save} style={{maxWidth: 640}}>

Edycja nagrania

set('title', e.target.value)} required />
set('youtube_id', e.target.value)} placeholder="np. dQw4w9WgXcQ" />
set('city', e.target.value)} />
setD(o => ({ ...o, lat, lng }))} />
set('lat', e.target.value)} />
set('lng', e.target.value)} />
set('tags', e.target.value)} />
set('duration', e.target.value)} placeholder="1:24" />
set('recorded_on', e.target.value)} />
set('start_at', e.target.value)} placeholder="np. 26 lub 0:26" />
set('end_at', e.target.value)} placeholder="np. 1:12" />