// events.jsx — Papan Event komunitas. Browse + detail + RSVP + buat acara. Global.
const EVENT_CATS = [
{ key: 'komunitas', label: 'Kumpul Warga', icon: 'users', color: '#7C3AED' },
{ key: 'olahraga', label: 'Olahraga', icon: 'dumbbell', color: '#10B981' },
{ key: 'bazaar', label: 'Bazaar', icon: 'store', color: '#EF4444' },
{ key: 'edukasi', label: 'Edukasi', icon: 'school', color: '#1B4FD8' },
{ key: 'hiburan', label: 'Hiburan', icon: 'music', color: '#EC4899' },
{ key: 'anak', label: 'Anak & Keluarga', icon: 'cake', color: '#F59E0B' },
{ key: 'keagamaan', label: 'Keagamaan', icon: 'leaf', color: '#14B8A6' },
{ key: 'lainnya', label: 'Lainnya', icon: 'grid', color: '#64748B' },
];
const EVENT_CAT = EVENT_CATS.reduce((a, c) => (a[c.key] = c, a), {});
const ID_DAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'];
const ID_MON = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
function evDateParts(iso) {
const d = new Date(iso);
return { day: d.getDate(), mon: ID_MON[d.getMonth()], dow: ID_DAYS[d.getDay()],
time: ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2), d: d };
}
function evWhenLabel(iso) {
const p = evDateParts(iso);
const now = new Date(); const t0 = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const diff = Math.round((new Date(p.d.getFullYear(), p.d.getMonth(), p.d.getDate()) - t0) / 86400000);
const rel = diff === 0 ? 'Hari ini' : diff === 1 ? 'Besok' : (p.dow + ', ' + p.day + ' ' + p.mon);
return rel + ' · ' + p.time;
}
function EventCard({ ev, onOpen }) {
const cc = EVENT_CAT[ev.category] || EVENT_CAT.lainnya;
const p = evDateParts(ev.starts_at);
return (
);
}
function EventsScreen({ authed, onRequireAuth, profile, clusters = [], onCreate }) {
const [items, setItems] = React.useState([]);
const [cat, setCat] = React.useState('all');
const [loading, setLoading] = React.useState(true);
const [rsvps, setRsvps] = React.useState({});
const [detail, setDetail] = React.useState(null);
const load = React.useCallback(() => {
setLoading(true);
gsData.loadEvents({ category: cat }).then(d => { setItems(d); setLoading(false); });
if (authed) gsData.myEventRsvps().then(setRsvps);
}, [cat, authed]);
React.useEffect(() => { load(); }, [load]);
return (
Acara Warga
setCat('all')}>Semua
{EVENT_CATS.map(c =>
setCat(c.key)}>{c.label})}
{loading ? (
) : items.length === 0 ? (
Belum ada acara mendatang
Buat acara pertama untuk warga!
) : (
{items.map(ev => )}
)}
{detail &&
setDetail(null)} onChanged={() => { setDetail(null); load(); }} />}
);
}
function EventDetailSheet({ ev, authed, profile, clusters = [], going, onRequireAuth, onClose, onChanged }) {
const cc = EVENT_CAT[ev.category] || EVENT_CAT.lainnya;
const [isGoing, setIsGoing] = React.useState(going);
const [count, setCount] = React.useState(ev.rsvp_count || 0);
const [busy, setBusy] = React.useState(false);
const [editing, setEditing] = React.useState(false);
const [confirmDel, setConfirmDel] = React.useState(false);
const isOwner = profile && ev.user_id === profile.id;
if (editing) return setEditing(false)} onDone={() => { setEditing(false); onChanged(); }} />
;
const wa = ev.contact_wa ? waLink(ev.contact_wa, 'Halo, saya mau tanya soal acara "' + ev.title + '" di gs.town.') : null;
const mapUrl = ev.lat != null && ev.lng != null
? 'https://www.google.com/maps/search/?api=1&query=' + ev.lat + ',' + ev.lng
: (ev.venue ? 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent(ev.venue + ' Gading Serpong') : null);
const toggle = async () => {
if (!authed) return onRequireAuth && onRequireAuth();
const next = !isGoing; setIsGoing(next); setCount(c => c + (next ? 1 : -1)); setBusy(true);
const res = await gsData.toggleRsvp(ev.id, next); setBusy(false);
if (res.error) { setIsGoing(!next); setCount(c => c + (next ? -1 : 1)); }
};
const ownAct = async (fn) => { setBusy(true); await fn(); setBusy(false); onChanged(); };
const p = evDateParts(ev.starts_at);
const endTime = ev.ends_at ? evDateParts(ev.ends_at).time : null;
return (
{ev.photo_url ?

:
}
{cc.label}
{ev.title}
{p.dow}, {p.day} {p.mon}
{p.time}{endTime ? ' – ' + endTime : ''}
{ev.venue && (
)}
{ev.organizer}
Penyelenggara{ev.cluster ? ' · ' + ev.cluster : ''}
{count}
{ev.body &&
{ev.body}
}
{isOwner ? (
{ev.status === 'cancelled' &&
Acara ini dibatalkan
}
{ev.status !== 'cancelled' && }
{confirmDel &&
setConfirmDel(false)} onConfirm={() => { setConfirmDel(false); ownAct(() => gsData.deleteEvent(ev.id)); }} />}
) : (
{wa && (authed
?
:
)}
)}
);
}
function EventCreateFlow({ clusters, profile, onClose, onDone }) {
const [cat, setCat] = React.useState('komunitas');
const [title, setTitle] = React.useState('');
const [starts, setStarts] = React.useState('');
const [ends, setEnds] = React.useState('');
const [venue, setVenue] = React.useState('');
const [clusterId, setClusterId] = React.useState('');
const [wa, setWa] = React.useState('');
const [body, setBody] = React.useState('');
const [photoFile, setPhotoFile] = React.useState(null);
const [photoPrev, setPhotoPrev] = React.useState(null);
const [err, setErr] = React.useState('');
const [busy, setBusy] = React.useState(false);
const [done, setDone] = React.useState(false);
const fileRef = React.useRef(null);
const onFile = (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; setPhotoFile(f); if (photoPrev) URL.revokeObjectURL(photoPrev); setPhotoPrev(URL.createObjectURL(f)); };
const submit = async () => {
if (!title.trim()) return setErr('Judul acara wajib diisi');
if (!starts) return setErr('Tanggal & waktu mulai wajib diisi');
setBusy(true); setErr('');
const res = await gsData.submitEvent({ category: cat, title, starts_at: new Date(starts).toISOString(),
ends_at: ends ? new Date(ends).toISOString() : null, venue, cluster_id: clusterId || null, contact_wa: wa, body, photoFile });
setBusy(false);
if (res.error) return setErr('Gagal: ' + res.error);
setDone(true); setTimeout(onDone, 1400);
};
if (done) return (
Acara dibuat!
Acaramu sudah tayang di papan acara warga.
);
const fld = { width: '100%', padding: '13px 14px', borderRadius: 'var(--r-md)', border: '1px solid var(--border)', background: 'var(--surface-2)', fontFamily: 'var(--font)', fontSize: 14.5, fontWeight: 500, color: 'var(--text-1)', outline: 'none' };
return (
{busy ? 'Membuat...' : 'Buat Acara'}{!busy && }}>
Kategori
{EVENT_CATS.map(c => { const on = cat === c.key; return (
); })}
Judul acara { setTitle(e.target.value); setErr(''); }} placeholder="cth. Senam Pagi Bersama Warga" />
Lokasi / venue setVenue(e.target.value)} placeholder="cth. Lapangan Cluster Allevare" />
Cluster (opsional)
Deskripsi (opsional)
Kontak WhatsApp (opsional) setWa(e.target.value)} placeholder="08xxxxxxxxxx" type="tel" />
{err &&
{err}
}
);
}
function toLocalInput(iso) {
if (!iso) return '';
const d = new Date(iso);
const p = n => ('0' + n).slice(-2);
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes());
}
function EventEditFlow({ ev, clusters = [], onClose, onDone }) {
const [cat, setCat] = React.useState(ev.category || 'komunitas');
const [title, setTitle] = React.useState(ev.title || '');
const [starts, setStarts] = React.useState(toLocalInput(ev.starts_at));
const [ends, setEnds] = React.useState(toLocalInput(ev.ends_at));
const [venue, setVenue] = React.useState(ev.venue || '');
const [clusterId, setClusterId] = React.useState(ev.cluster_id || '');
const [wa, setWa] = React.useState(ev.contact_wa || '');
const [body, setBody] = React.useState(ev.body || '');
const [photoFile, setPhotoFile] = React.useState(null);
const [photoPrev, setPhotoPrev] = React.useState(ev.photo_url || null);
const [err, setErr] = React.useState('');
const [busy, setBusy] = React.useState(false);
const fileRef = React.useRef(null);
const onFile = (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; setPhotoFile(f); setPhotoPrev(URL.createObjectURL(f)); };
const submit = async () => {
if (!title.trim()) return setErr('Judul acara wajib diisi');
if (!starts) return setErr('Tanggal & waktu mulai wajib diisi');
setBusy(true); setErr('');
const res = await gsData.updateEvent(ev.id, { category: cat, title, starts_at: new Date(starts).toISOString(),
ends_at: ends ? new Date(ends).toISOString() : null, venue, cluster_id: clusterId || null, contact_wa: wa, body, photoFile });
setBusy(false);
if (res.error) return setErr('Gagal: ' + res.error);
onDone();
};
const fld = { width: '100%', padding: '13px 14px', borderRadius: 'var(--r-md)', border: '1px solid var(--border)', background: 'var(--surface-2)', fontFamily: 'var(--font)', fontSize: 14.5, fontWeight: 500, color: 'var(--text-1)', outline: 'none' };
return (
{busy ? 'Menyimpan...' : 'Simpan perubahan'}{!busy && }}>
Kategori
{EVENT_CATS.map(c => { const on = cat === c.key; return (
); })}
Judul acara { setTitle(e.target.value); setErr(''); }} placeholder="cth. Senam Pagi Bersama Warga" />
Lokasi / venue setVenue(e.target.value)} placeholder="cth. Lapangan Cluster Allevare" />
Cluster (opsional)
Deskripsi
Kontak WhatsApp setWa(e.target.value)} placeholder="08xxxxxxxxxx" type="tel" />
{err &&
{err}
}
);
}
Object.assign(window, { EventsScreen, EventDetailSheet, EventCreateFlow, EventEditFlow, EVENT_CATS, evDateParts, evWhenLabel });