import { useState, useEffect, useCallback } from "react";
const API = "https://3marketplace.shop";
const COLORS = {
bg: "#0a0a0f", surface: "#12121a", card: "#1a1a26", border: "#2a2a3d",
accent: "#4f6ef7", gold: "#f0b429", green: "#22c55e", red: "#ef4444",
cyan: "#06b6d4", text: "#e8e8f0", muted: "#6b6b8a",
};
const CATEGORIES = ["all", "Electronics", "Fashion", "Gaming", "Sports", "Home", "Books", "Cars", "Other"];
const CONDITIONS = ["new", "like_new", "good", "used", "for_parts"];
const api = {
post: async (path, body, token) => {
const res = await fetch(`${API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify(body),
});
return res.json();
},
get: async (path, token) => {
const res = await fetch(`${API}${path}`, {
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
});
return res.json();
},
delete: async (path, token) => {
const res = await fetch(`${API}${path}`, {
method: "DELETE",
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
});
return res.json();
},
};
const formatEur = (n) => new Intl.NumberFormat("hr-HR", { style: "currency", currency: "EUR" }).format(n || 0);
const s = {
root: { minHeight: "100vh", background: COLORS.bg, color: COLORS.text, fontFamily: "'DM Sans', sans-serif" },
glow: { position: "fixed", top: -200, left: "50%", transform: "translateX(-50%)", width: 800, height: 400, background: "radial-gradient(ellipse, #4f6ef718 0%, transparent 70%)", pointerEvents: "none", zIndex: 0 },
wrap: { maxWidth: 1100, margin: "0 auto", padding: "0 20px", position: "relative", zIndex: 1 },
card: { background: COLORS.card, border: `1px solid ${COLORS.border}`, borderRadius: 16, padding: 20 },
btn: (color = COLORS.accent, outline = false) => ({ background: outline ? "transparent" : color, color: outline ? color : "#fff", border: `1.5px solid ${color}`, borderRadius: 10, padding: "10px 20px", cursor: "pointer", fontSize: 13, fontWeight: 700, transition: "all 0.18s" }),
input: { background: COLORS.surface, border: `1.5px solid ${COLORS.border}`, borderRadius: 10, padding: "11px 14px", color: COLORS.text, fontSize: 14, outline: "none", width: "100%", boxSizing: "border-box" },
};
function Notification({ msg, type }) {
if (!msg) return null;
const color = type === "error" ? COLORS.red : COLORS.green;
return (
{msg}
);
}
// ─── AUTH ───
function AuthScreen({ onLogin, notify }) {
const [mode, setMode] = useState("login");
const [form, setForm] = useState({ email: "", password: "", username: "", location: "Croatia" });
const [loading, setLoading] = useState(false);
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
const submit = async () => {
if (!form.email || !form.password) return notify("Email and password required", "error");
setLoading(true);
try {
if (mode === "register") {
const res = await api.post("/auth/register", form);
if (res.error) return notify(res.error, "error");
notify("Account created! Please log in.");
setMode("login");
} else {
const res = await api.post("/auth/login", { email: form.email, password: form.password });
if (res.error) return notify(res.error, "error");
localStorage.setItem("3m_token", res.token);
localStorage.setItem("3m_user", JSON.stringify(res.user));
onLogin(res.user, res.token);
}
} catch { notify("Connection error", "error"); }
finally { setLoading(false); }
};
return (
);
}
// ─── LISTING CARD ───
function ListingCard({ listing, onBuy, isOwner, onDelete }) {
const cashbackPct = listing.xmr_accepted ? 5 : 2;
const cashback = parseFloat((listing.price * cashbackPct / 100).toFixed(2));
return (
{isOwner && (
)}
{listing.images?.[0] ?

: "📦"}
{listing.title}
{listing.seller_username} · {listing.location}
{listing.category}
{listing.condition?.replace("_", " ")}
{listing.xmr_accepted && ⊛ XMR}
{listing.allow_barter && ↔ Barter}
{listing.description &&
{listing.description.slice(0, 100)}{listing.description.length > 100 ? "..." : ""}
}
{formatEur(listing.price)}
+{formatEur(cashback)} cashback ({cashbackPct}%)
{!isOwner &&
}
);
}
// ─── NEW LISTING FORM ───
function NewListingForm({ token, onCreated, notify, onClose }) {
const [form, setForm] = useState({ title: "", description: "", price: "", category: "Electronics", condition: "used", location: "", allow_barter: false, xmr_accepted: false });
const [loading, setLoading] = useState(false);
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.type === "checkbox" ? e.target.checked : e.target.value }));
const submit = async () => {
if (!form.title || !form.price) return notify("Title and price required", "error");
setLoading(true);
const res = await api.post("/listings", form, token);
setLoading(false);
if (res.error) return notify(res.error, "error");
notify("Listing published! 🎉");
onCreated();
onClose();
};
return (
);
}
// ─── MARKET TAB ───
function MarketTab({ user, token, notify }) {
const [listings, setListings] = useState([]);
const [loading, setLoading] = useState(true);
const [category, setCategory] = useState("all");
const [search, setSearch] = useState("");
const [showNew, setShowNew] = useState(false);
const [buying, setBuying] = useState(null);
const load = useCallback(async () => {
setLoading(true);
const params = new URLSearchParams({ limit: 50 });
if (category !== "all") params.set("category", category);
if (search) params.set("search", search);
const res = await api.get(`/listings?${params}`);
setListings(res.listings || []);
setLoading(false);
}, [category, search]);
useEffect(() => { load(); }, [load]);
const handleBuy = async (listing) => {
const res = await api.post("/create-checkout-session", {
listingId: listing.id, title: listing.title, price: listing.price,
userId: user.id, isXmrUser: listing.xmr_accepted,
imageUrl: listing.images?.[0]
}, token);
if (res.error) return notify(res.error, "error");
if (res.url) window.open(res.url, "_blank");
};
const handleDelete = async (id) => {
const res = await api.delete(`/listings/${id}`, token);
if (res.error) return notify(res.error, "error");
notify("Listing removed");
load();
};
return (
{showNew &&
setShowNew(false)} />}
{/* Search + filter bar */}
setSearch(e.target.value)} />
{/* Category pills */}
{CATEGORIES.map((c) => (
))}
{loading && Loading listings...
}
{!loading && listings.length === 0 && (
📭
No listings yet
Be the first to sell something on Tri Kolektiv!
)}
{listings.map((l) => (
))}
);
}
// ─── WALLET TAB ───
function WalletTab({ user, token, notify }) {
const [wallet, setWallet] = useState(null);
const [loading, setLoading] = useState(true);
const [withdrawAmt, setWithdrawAmt] = useState("");
const load = useCallback(async () => {
setLoading(true);
const data = await api.get(`/wallet/${user.id}`, token);
setWallet(data);
setLoading(false);
}, [user.id, token]);
useEffect(() => { load(); }, [load]);
const withdraw = async () => {
const amt = parseFloat(withdrawAmt);
if (!amt || amt < 5) return notify("Min withdrawal is €5", "error");
const res = await api.post("/wallet/withdraw", { userId: user.id, amount: amt }, token);
if (res.error) return notify(res.error, "error");
notify(`€${amt} withdrawal requested ✅`);
setWithdrawAmt("");
load();
};
if (loading) return Loading wallet...
;
return (
CASHBACK BALANCE
{formatEur(wallet?.balance)}
Lifetime: {formatEur(wallet?.lifetime_earned)}
WITHDRAW
setWithdrawAmt(e.target.value)} />
Transfers to your IBAN
Transaction History
{!wallet?.history?.length &&
No transactions yet — start buying to earn cashback! 💰
}
{wallet?.history?.map((tx) => (
{tx.description}
{new Date(tx.created_at).toLocaleDateString("hr-HR")}
0 ? COLORS.green : COLORS.red }}>{tx.amount > 0 ? "+" : ""}{formatEur(tx.amount)}
))}
);
}
// ─── MAIN APP ───
export default function App() {
const [user, setUser] = useState(() => { try { return JSON.parse(localStorage.getItem("3m_user")); } catch { return null; } });
const [token, setToken] = useState(() => localStorage.getItem("3m_token") || null);
const [tab, setTab] = useState("market");
const [notification, setNotification] = useState(null);
const [backendOnline, setBackendOnline] = useState(null);
const notify = (msg, type = "success") => {
setNotification({ msg, type });
setTimeout(() => setNotification(null), 3500);
};
useEffect(() => {
api.get("/").then((r) => setBackendOnline(!!r.status)).catch(() => setBackendOnline(false));
if (token) {
api.get("/auth/me", token).then((r) => {
if (r.error) { localStorage.removeItem("3m_token"); localStorage.removeItem("3m_user"); setUser(null); setToken(null); }
else { setUser(r); localStorage.setItem("3m_user", JSON.stringify(r)); }
});
}
}, [token]);
const logout = () => {
api.post("/auth/logout", {}, token);
localStorage.removeItem("3m_token"); localStorage.removeItem("3m_user");
setUser(null); setToken(null);
};
const onLogin = (u, t) => { setUser(u); setToken(t); notify(`Dobrodošao, ${u.username || u.email}! 👋`); };
if (!user) return (<>>);
return (
{/* Header */}
🛒
3Market
Tri Kolektiv
{backendOnline !== null && (
{backendOnline ? "● Live" : "⚠ Offline"}
)}
👤 {user.username || user.email}
{/* Tabs */}
{["market", "wallet", "profile"].map((t) => (
))}
{/* Content */}
{tab === "market" &&
}
{tab === "wallet" &&
}
{tab === "profile" && (
Profile
{[["Email", user.email], ["Username", user.username || "—"], ["Location", user.location || "—"], ["XMR user", user.monero_user ? "✅ Yes (+5% cashback)" : "No"]].map(([l, v]) => (
{l}
{v}
))}
)}
);
}