// 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
{stats.map(([k, v]) => (
))}
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();