// app.jsx — gs.town production shell: public browse, login-on-action, live data. const NAV = [ { key: 'home', label: 'Home', icon: 'home' }, { key: 'map', label: 'Peta', icon: 'map' }, { key: 'report', label: 'Lapor', icon: 'plus', center: true }, { key: 'cluster', label: 'Cluster', icon: 'shield' }, { key: 'profile', label: 'Profil', icon: 'user' }, ]; function BottomNav({ tab, onTab, onReport }) { return (
{NAV.map(n => { if (n.center) { return ( ); } const on = tab === n.key; return ( ); })}
); } function initials(name) { if (!name) return 'GS'; const p = name.trim().split(/\s+/); const a = (p[0] || '')[0] || 'G'; const b = p[1] ? p[1][0] : ''; return (a + b).toUpperCase(); } function GuestProfile({ onLogin }) { return (

Profil

Gabung warga GS

Masuk untuk melapor, berkomentar, dukung laporan, dan kumpulkan poin.

); } function Profile({ profile, myReports, onSignOut, openReport }) { const name = (profile && profile.full_name) || 'Warga GS'; const lvl = (profile && profile.level) || 'Warga Baru'; const pts = (profile && profile.points) || 0; const stats = [['Laporan', myReports.length], ['Poin', pts], ['Level', lvl]]; return (

Profil

{initials(name)}
{name}
{lvl}
{stats.map(([k, v]) => (
{v}
{k}
))}

Laporan saya

{myReports.length === 0 ? (

Belum ada laporan. Mulai lapor untuk dapat poin!

) : (
{myReports.map(r => {}} upActive={false} />)}
)}
); } function App() { const t = { showSponsored: true, mapVignette: true }; const [authed, setAuthed] = React.useState(null); // null=checking, false=guest, true=member const [profile, setProfile] = React.useState(null); const [tab, setTab] = React.useState('home'); const [reports, setReports] = React.useState([]); const [places, setPlaces] = React.useState([]); const [clusters, setClusters] = React.useState([]); const [myUps, setMyUps] = React.useState({}); const [myReps, setMyReps] = React.useState([]); const [loading, setLoading] = React.useState(true); const [reportFlow, setReportFlow] = React.useState(null); const [detailReport, setDetailReport] = React.useState(null); const [detailPlace, setDetailPlace] = React.useState(null); const [authPrompt, setAuthPrompt] = React.useState(false); React.useEffect(() => { document.documentElement.setAttribute('data-theme', 'light'); }, []); React.useEffect(() => { gsSupabase.auth.getSession().then(({ data }) => setAuthed(!!data.session)); const { data: sub } = gsSupabase.auth.onAuthStateChange((_e, session) => setAuthed(!!session)); return () => sub.subscription.unsubscribe(); }, []); const refreshAll = React.useCallback(async () => { setLoading(true); const [rep, pl, cl, up, prof, mine] = await Promise.all([ gsData.loadReports(), gsData.loadPlaces(), gsData.loadClusters(), gsData.myUpvotes(), gsData.getProfile(), gsData.myReports(), ]); setReports(rep); setPlaces(pl); setClusters(cl); setMyUps(up); setProfile(prof); setMyReps(mine); setLoading(false); }, []); // load data for everyone (anon read allowed); re-run when auth flips React.useEffect(() => { if (authed !== null) refreshAll(); }, [authed, refreshAll]); // realtime reports for all visitors React.useEffect(() => { const ch = gsSupabase.channel('gst_reports_rt') .on('postgres_changes', { event: '*', schema: 'public', table: 'gst_reports' }, () => { gsData.loadReports().then(setReports); }).subscribe(); return () => { gsSupabase.removeChannel(ch); }; }, []); const requireAuth = () => { if (!authed) { setAuthPrompt(true); return false; } return true; }; const onUp = async (id) => { if (!requireAuth()) return; const newOn = !myUps[id]; setMyUps(p => ({ ...p, [id]: newOn })); setReports(rs => rs.map(r => r.id === id ? { ...r, up: r.up + (newOn ? 1 : -1) } : r)); const res = await gsData.toggleUpvote(id, newOn); if (res && res.error) { setMyUps(p => ({ ...p, [id]: !newOn })); setReports(rs => rs.map(r => r.id === id ? { ...r, up: r.up + (newOn ? -1 : 1) } : r)); } }; const go = (k) => { setTab(k); setDetailReport(null); setDetailPlace(null); }; const openReport = (r) => setDetailReport(r); const openPlace = (p) => setDetailPlace(p); const startReport = (cat) => { if (!requireAuth()) return; setReportFlow({ cat: typeof cat === 'string' ? cat : null }); }; if (authed === null && loading) { return
; } let screen; if (tab === 'home') screen = ; else if (tab === 'map') screen = startReport()} openReport={openReport} />; else if (tab === 'cluster') screen = setAuthPrompt(true)} clusters={clusters} profile={profile} />; else if (tab === 'directory') screen = ; else screen = authed ? { await gsData.signOut(); setAuthed(false); }} openReport={openReport} /> : setAuthPrompt(true)} />; return (
{screen} startReport()} /> {detailPlace && setDetailPlace(null)} />} {detailReport && setAuthPrompt(true)} onClose={() => setDetailReport(null)} />} {reportFlow && (
setReportFlow(null)} onDone={() => { setReportFlow(null); setTab('map'); refreshAll(); }} />
)} {authPrompt && (
setAuthPrompt(false)} onAuthed={() => { setAuthPrompt(false); setAuthed(true); refreshAll(); }} />
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();