// 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" />
Deskripsi (opsional)
Nomor WhatsApp { setWa(e.target.value); setErr(''); }} placeholder="08xxxxxxxxxx" type="tel" />
Cluster (opsional)
{err &&
{err}
}
);
}
function LapakUpgrade({ quota, note, onClose }) {
const [cfg, setCfg] = React.useState({});
React.useEffect(() => { gsData.loadSettings().then(setCfg); }, []);
const priceFor = (tier) => {
if (tier === 2) return cfg.lapak_price_t2 != null ? cfg.lapak_price_t2 : 50000;
if (tier === 3) return cfg.lapak_price_t3 != null ? cfg.lapak_price_t3 : 75000;
if (tier === 4) return cfg.lapak_price_t4 != null ? cfg.lapak_price_t4 : 120000;
return 0;
};
const pengurusWa = cfg.pengurus_wa ? waLink(cfg.pengurus_wa, 'Halo, saya mau upgrade paket Lapak di gs.town.') : null;
return (
{note &&
{note}
}
{LAPAK_TIERS.map(t => { const cur = quota && quota.tier === t.tier; return (
{t.name}
{cur && PAKET KAMU}
{t.quota} · {t.sub}
1 ? 'var(--cta)' : 'var(--lime-700)' }}>{t.tier > 1 ? 'Rp' + priceFor(t.tier).toLocaleString('id-ID') : 'Gratis'}
{t.tier > 1 &&
/bulan
}
); })}
Pembayaran online segera hadir. Untuk upgrade sekarang, hubungi pengurus.
{pengurusWa
?
Hubungi pengurus{cfg.pengurus_name ? ' · ' + cfg.pengurus_name : ''}
: null}
);
}
// ---- edit existing lapak (prefilled) ----
function LapakEditFlow({ item, clusters = [], onClose, onDone }) {
const [cat, setCat] = React.useState(item.category || 'preloved');
const [title, setTitle] = React.useState(item.title || '');
const [price, setPrice] = React.useState(item.price == null ? '' : String(item.price));
const [body, setBody] = React.useState(item.body || '');
const [wa, setWa] = React.useState(item.contact_wa || '');
const [clusterId, setClusterId] = React.useState(item.cluster_id || '');
const [keep, setKeep] = React.useState(Array.isArray(item.photos) ? item.photos.slice() : []);
const [files, setFiles] = React.useState([]);
const [prev, setPrev] = React.useState([]);
const [err, setErr] = React.useState('');
const [busy, setBusy] = React.useState(false);
const fileRef = React.useRef(null);
const onFiles = (e) => {
const room = 4 - keep.length - files.length;
const fs = Array.from(e.target.files || []).slice(0, Math.max(0, room));
if (!fs.length) return;
setFiles(p => [...p, ...fs]);
setPrev(p => [...p, ...fs.map(f => URL.createObjectURL(f))]);
};
const rmKeep = (i) => setKeep(p => p.filter((_, j) => j !== i));
const rmNew = (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.updateLapak(item.id, { category: cat, title, price, body, contact_wa: wa, cluster_id: clusterId || null, files, keepPhotos: keep });
setBusy(false);
if (res.error) return setErr('Gagal: ' + res.error);
onDone();
};
return (
{busy ? 'Menyimpan...' : 'Simpan perubahan'}{!busy && }}>
Kategori
{LAPAK_CATS.map(c => { const on = cat === c.key; return (
); })}
Foto (maks 4)
{keep.map((src, i) => (
))}
{prev.map((src, i) => (
))}
{(keep.length + files.length) < 4 &&
}
Judul { setTitle(e.target.value); setErr(''); }} placeholder="cth. iPhone 13 128GB mulus" />
Deskripsi
Nomor WhatsApp { setWa(e.target.value); setErr(''); }} placeholder="08xxxxxxxxxx" type="tel" />
Cluster (opsional)
{err &&
{err}
}
);
}
// ---- shared flow shell (fullscreen) ----
function FlowShell({ title, children, footer, onClose }) {
return (
{children}
{footer &&
{footer}
}
);
}
const inpStyle = { 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' };
function Lbl({ children }) { return ; }
function Inp(props) { return ; }
Object.assign(window, { LapakScreen, LapakPostFlow, LapakUpgrade, LapakDetailSheet, LapakEditFlow, LAPAK_CATS, LAPAK_TIERS, lapakRp, waLink });