// lapak.jsx — Lapak warga (marketplace). Browse + detail + pasang (quota/tier). Global. const LAPAK_CATS = [ { key: 'preloved', label: 'Pre-loved', icon: 'tag', color: '#7C3AED' }, { key: 'properti', label: 'Properti', icon: 'home', color: '#1B4FD8' }, { key: 'jasa', label: 'Jasa & Tukang', icon: 'wrench', color: '#F59E0B' }, { key: 'loker', label: 'Loker', icon: 'bag', color: '#0EA5E9' }, { key: 'kuliner', label: 'Kuliner', icon: 'food', color: '#EF4444' }, { key: 'otomotif', label: 'Otomotif', icon: 'car', color: '#14B8A6' }, { key: 'jastip', label: 'Jastip', icon: 'store', color: '#EC4899' }, { key: 'hobi', label: 'Hobi & OR', icon: 'dumbbell', color: '#10B981' }, { key: 'lainnya', label: 'Lainnya', icon: 'grid', color: '#64748B' }, ]; const LAPAK_CAT = LAPAK_CATS.reduce((a, c) => (a[c.key] = c, a), {}); const LAPAK_TIERS = [ { tier: 1, name: 'Gratis', price: 0, quota: '1 lapak / bulan', sub: 'Untuk semua warga' }, { tier: 2, name: 'Mingguan', price: 50000, quota: '1 lapak / minggu', sub: 'Lebih sering jualan' }, { tier: 3, name: 'Pro', price: 75000, quota: '2 lapak / minggu', sub: 'Untuk yang aktif' }, { tier: 4, name: 'Bisnis', price: 120000, quota: '3 lapak / minggu', sub: 'Untuk UMKM & toko' }, ]; function lapakRp(n) { if (n == null || n === '') return 'Nego'; return 'Rp' + Number(n).toLocaleString('id-ID'); } function waLink(num, text) { if (!num) return null; var n = ('' + num).replace(/[^0-9]/g, ''); if (n.indexOf('0') === 0) n = '62' + n.slice(1); else if (n.indexOf('62') !== 0) n = '62' + n; return 'https://wa.me/' + n + (text ? '?text=' + encodeURIComponent(text) : ''); } function LapakCard({ item, onOpen }) { const cc = LAPAK_CAT[item.category] || LAPAK_CAT.lainnya; const img = item.photos && item.photos[0]; return ( ); } function LapakScreen({ authed, onRequireAuth, clusters, profile, onPost }) { const [items, setItems] = React.useState([]); const [cat, setCat] = React.useState('all'); const [q, setQ] = React.useState(''); const [loading, setLoading] = React.useState(true); const [detail, setDetail] = React.useState(null); const load = React.useCallback(() => { setLoading(true); gsData.loadLapak({ category: cat, search: q.trim() }).then(d => { setItems(d); setLoading(false); }); }, [cat, q]); React.useEffect(() => { const t = setTimeout(load, q ? 280 : 0); return () => clearTimeout(t); }, [cat, q]); const isOwner = detail && profile && detail.user_id === profile.id; return (

Lapak Warga

setQ(e.target.value)} placeholder="Cari barang, jasa, properti..." style={{ width: '100%', padding: '11px 14px 11px 38px', borderRadius: 'var(--r-chip)', border: '1px solid var(--border)', background: 'var(--surface-2)', fontFamily: 'var(--font)', fontSize: 14, color: 'var(--text-1)', outline: 'none' }} />
setCat('all')}>Semua {LAPAK_CATS.map(c => setCat(c.key)}>{c.label})}
{loading ? (
{[0, 1, 2, 3].map(i =>
)}
) : items.length === 0 ? (
Belum ada lapak di sini
Jadi yang pertama pasang lapak!
) : (
{items.map(it => )}
)}
{detail && setDetail(null)} onChanged={() => { setDetail(null); load(); }} />}
); } function LapakDetailSheet({ item, isOwner, authed, clusters = [], onRequireAuth, onClose, onChanged }) { const cc = LAPAK_CAT[item.category] || LAPAK_CAT.lainnya; const [idx, setIdx] = React.useState(0); const [busy, setBusy] = React.useState(false); const [editing, setEditing] = React.useState(false); const [confirmDel, setConfirmDel] = React.useState(false); const photos = item.photos && item.photos.length ? item.photos : []; const wa = waLink(item.contact_wa, 'Halo, saya lihat lapak "' + item.title + '" di gs.town. Masih ada?'); const act = async (fn) => { setBusy(true); await fn(); setBusy(false); onChanged(); }; if (editing) return
setEditing(false)} onDone={() => { setEditing(false); onChanged(); }} />
; return (
{photos.length ? : } {photos.length > 1 && (
{photos.map((_, i) =>
)}
{cc.label}

{item.title}

{lapakRp(item.price)}
{item.body &&

{item.body}

}
{item.seller}
{item.cluster &&
{item.cluster}
}
{isOwner ? (
Kelola lapak kamu
{item.status !== 'sold' && }
{confirmDel && setConfirmDel(false)} onConfirm={() => { setConfirmDel(false); act(() => gsData.deleteLapak(item.id)); }} />}
) : ( !item.contact_wa ?
Penjual tidak mencantumkan kontak.
: !authed ? ( ) : ( Hubungi via WhatsApp ) )}
); } function ownBtn(color) { return { flex: 1, padding: '13px', borderRadius: 'var(--r-md)', border: '1px solid var(--border)', background: 'var(--surface)', color: color, fontWeight: 700, fontSize: 13.5, cursor: 'pointer', fontFamily: 'var(--font)' }; } function LapakPostFlow({ clusters, profile, onClose, onDone }) { const [quota, setQuota] = React.useState(undefined); const [cat, setCat] = React.useState('preloved'); const [title, setTitle] = React.useState(''); const [price, setPrice] = React.useState(''); const [body, setBody] = React.useState(''); const [wa, setWa] = React.useState(''); const [clusterId, setClusterId] = React.useState(''); const [files, setFiles] = React.useState([]); const [prev, setPrev] = React.useState([]); const [err, setErr] = React.useState(''); const [busy, setBusy] = React.useState(false); const [done, setDone] = React.useState(false); const [view, setView] = React.useState('form'); const fileRef = React.useRef(null); React.useEffect(() => { gsData.lapakQuota().then(setQuota); }, []); const onFiles = (e) => { const fs = Array.from(e.target.files || []).slice(0, 4 - files.length); if (!fs.length) return; setFiles(p => [...p, ...fs].slice(0, 4)); setPrev(p => [...p, ...fs.map(f => URL.createObjectURL(f))].slice(0, 4)); }; const rmPhoto = (i) => { setFiles(p => p.filter((_, j) => j !== i)); setPrev(p => p.filter((_, j) => j !== i)); }; const submit = async () => { if (!title.trim()) return setErr('Judul lapak wajib diisi'); if (!wa.trim()) return setErr('Nomor WhatsApp wajib diisi'); setBusy(true); setErr(''); const res = await gsData.submitLapak({ category: cat, title, price, body, contact_wa: wa, cluster_id: clusterId || null, files }); setBusy(false); if (res.error === 'QUOTA') { setQuota(q => ({ ...(q || {}), can_post: false })); return; } if (res.error) return setErr('Gagal: ' + res.error); setDone(true); setTimeout(onDone, 1400); }; if (done) { return

Lapak terpasang!

Lapakmu sudah tayang dan bisa dilihat semua warga.

; } if (view === 'upgrade') { return setView('form')} title="Paket Lapak"> setView('form')} />; } // gate kuota habis -> tampilkan upsell tier if (quota && quota.can_post === false) { return ; } return ( {busy ? 'Memasang...' : 'Pasang Lapak'}{!busy && }}>
{quota &&
Sisa kuota: {quota.remaining} dari {quota.limit} ({quota.period === 'week' ? 'minggu ini' : 'bulan ini'})
}
Kategori
{LAPAK_CATS.map(c => { const on = cat === c.key; return ( ); })}
Foto (maks 4, opsional)
{prev.map((src, i) => (
))} {files.length < 4 && }
Judul { setTitle(e.target.value); setErr(''); }} placeholder="cth. iPhone 13 128GB mulus" />
Harga (kosongkan = nego)
Rp setPrice(e.target.value)} placeholder="0" style={{ ...inpStyle, paddingLeft: 40 }} />
Deskripsi (opsional)