// admin.jsx — área do vendedor: login, dashboard, CRUD form const AUTH_KEY = "moliterni_auth_v1"; // Híbrido: no modo "api" a sessão é validada pelo servidor (session.php / login.php); // no modo "demo" usa um flag local contra as credenciais chumbadas (ADMIN_CRED). function useAuth(mode) { const [authed, setAuthed] = React.useState(false); const [checking, setChecking] = React.useState(true); React.useEffect(() => { if (mode === "loading") return; let alive = true; if (mode === "api") { fetch(API_BASE + "session.php", { credentials: "include" }) .then((r) => r.json()) .then((d) => { if (alive) { setAuthed(!!d.authed); setChecking(false); } }) .catch(() => { if (alive) setChecking(false); }); } else { try { setAuthed(localStorage.getItem(AUTH_KEY) === "1"); } catch (e) {} setChecking(false); } return () => { alive = false; }; }, [mode]); const login = () => { if (mode === "demo") { try { localStorage.setItem(AUTH_KEY, "1"); } catch (e) {} } setAuthed(true); }; const logout = () => { if (mode === "api") { fetch(API_BASE + "logout.php", { credentials: "include" }).catch(() => {}); } else { try { localStorage.removeItem(AUTH_KEY); } catch (e) {} } setAuthed(false); }; return { authed, login, logout, checking }; } // ---------- Login ---------- function AdminLogin({ onLogin, onBack, mode }) { const [user, setUser] = React.useState(""); const [pass, setPass] = React.useState(""); const [err, setErr] = React.useState(""); const [busy, setBusy] = React.useState(false); const submit = async (e) => { e.preventDefault(); if (mode === "api") { setBusy(true); setErr(""); try { const r = await fetch(API_BASE + "login.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ usuario: user.trim(), senha: pass }), }); const d = await r.json(); if (r.ok && d.ok) onLogin(); else setErr(d.erro || "Usuário ou senha incorretos."); } catch (e) { setErr("Falha ao conectar no servidor."); } finally { setBusy(false); } } else { if (user.trim() === ADMIN_CRED.user && pass === ADMIN_CRED.pass) { setErr(""); onLogin(); } else setErr("Usuário ou senha incorretos."); } }; return (

Área do vendedor

Moliterni Motos
setUser(e.target.value)} placeholder="admin" autoFocus />
setPass(e.target.value)} placeholder="••••••••" />
{err &&
{err}
}
{mode !== "api" && (
Demonstração: usuário admin · senha demo123
)}
); } // ---------- Photo URL editor ---------- function PhotoEditor({ fotos, onChange, vehicle, mode }) { const [url, setUrl] = React.useState(""); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); const fileRef = React.useRef(null); const add = () => { if (url.trim()) { onChange([...(fotos || []), url.trim()]); setUrl(""); } }; const remove = (i) => onChange(fotos.filter((_, idx) => idx !== i)); const pickFile = async (e) => { const file = e.target.files && e.target.files[0]; e.target.value = ""; // permite reenviar o mesmo arquivo if (!file) return; setErr(""); if (mode === "api") { // envia pro servidor; o banco guarda só o caminho devolvido (fotos/xxx.jpg) setBusy(true); try { const fd = new FormData(); fd.append("foto", file); const r = await fetch(API_BASE + "upload.php", { method: "POST", credentials: "include", body: fd }); const d = await r.json(); if (r.ok && d.ok) onChange([...(fotos || []), d.caminho]); else setErr(d.erro || "Falha ao enviar a foto."); } catch (e) { setErr("Falha ao enviar a foto."); } finally { setBusy(false); } } else { // modo demo: sem servidor, guarda a imagem como data URL (fica no localStorage) const reader = new FileReader(); reader.onload = () => onChange([...(fotos || []), reader.result]); reader.readAsDataURL(file); } }; return (
{err &&
{err}
}
setUrl(e.target.value)} placeholder="…ou cole o link de uma foto (https://…)" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }} />
{(fotos || []).length === 0 && (
)} {(fotos || []).map((f, i) => (
{ e.target.style.opacity = .2; }} /> {i === 0 && CAPA}
))}

Sem foto? O anúncio mostra um placeholder elegante automaticamente. A primeira foto vira a capa.

); } // ---------- Vehicle form ---------- const EMPTY = { categoria: "street", marca: "", modelo: "", ano: new Date().getFullYear(), preco: "", precoAntigo: "", km: "", cambio: "Manual", combustivel: "Gasolina", cor: "", status: "disponivel", descricao: "", opcionais: [], fotos: [], destaque: false, }; function VehicleForm({ open, onClose, onSave, initial, mode }) { const [f, setF] = React.useState(EMPTY); const [custom, setCustom] = React.useState(""); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); React.useEffect(() => { if (open) { setF(initial ? { ...EMPTY, ...initial } : EMPTY); setErr(""); } }, [open, initial]); const set = (k, v) => setF((p) => ({ ...p, [k]: v })); const toggleOpc = (o) => setF((p) => ({ ...p, opcionais: p.opcionais.includes(o) ? p.opcionais.filter((x) => x !== o) : [...p.opcionais, o] })); const addCustom = () => { const v = custom.trim(); if (v && !f.opcionais.includes(v)) { set("opcionais", [...f.opcionais, v]); setCustom(""); } }; const valid = f.marca.trim() && f.modelo.trim() && f.preco; const save = async () => { if (!valid || busy) return; setBusy(true); setErr(""); try { await onSave({ ...f, ano: Number(f.ano) || new Date().getFullYear(), preco: Number(f.preco) || 0, precoAntigo: Number(f.precoAntigo) || 0, km: Number(f.km) || 0 }); } catch (e) { setErr(e.message || "Não foi possível salvar."); setBusy(false); } }; return (
{/* segmento */}
{SEGMENTOS.map((s) => ( ))}
set("marca", e.target.value)} placeholder="Ex: Honda" />
set("modelo", e.target.value)} placeholder="Ex: CB 500F" />
set("ano", e.target.value)} />
set("km", e.target.value)} placeholder="0" />
set("cor", e.target.value)} placeholder="Ex: Preto" />
set("preco", e.target.value)} placeholder="0" />
set("precoAntigo", e.target.value)} placeholder="(p/ oferta)" />