import { useState, useCallback, useEffect } from "react"; /* ── WordPress Site Manager ────────────────────────────────────── เชื่อมต่อ vanvaew.com (หรือเว็บ WP ใดๆ) ผ่าน REST API พร้อม Application Password → แก้ไข Posts, Pages, Homepage ──────────────────────────────────────────────────────────────── */ const CSS = ` @import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} :root{ --bg:#0f0e0c; --bg2:#181614; --bg3:#211f1c; --border:rgba(255,255,255,0.07); --border2:rgba(255,255,255,0.12); --text:#f0ece4; --muted:#888078; --sub:#5a5650; --green:#2ec27e; --green-d:#1a7a50; --green-bg:rgba(46,194,126,0.1); --amber:#e8a030; --amber-bg:rgba(232,160,48,0.1); --red:#e05050; --red-bg:rgba(224,80,80,0.1); --blue:#4a9eff; --blue-bg:rgba(74,158,255,0.08); --radius:12px; } body{background:var(--bg);color:var(--text);font-family:'Sarabun',sans-serif;font-size:14px;line-height:1.6} @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} @keyframes spin{to{transform:rotate(360deg)}} @keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}} .fade{animation:fadeUp .3s ease} ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent} ::-webkit-scrollbar-thumb{background:#333;border-radius:4px} input,textarea,select{font-family:'Sarabun',sans-serif;font-size:14px} button{font-family:'Sarabun',sans-serif;cursor:pointer} `; // ── Helpers const api = (base, path, opts = {}) => fetch(`${base}/wp-json/wp/v2${path}`, opts); const authHead = (user, pass) => ({ Authorization: "Basic " + btoa(`${user}:${pass}`), "Content-Type": "application/json", }); const Spin = () => (
); const Badge = ({ s }) => { const map = { publish: ["var(--green)", "เผยแพร่"], draft: ["var(--amber)", "ร่าง"], private: ["#a78bfa", "ส่วนตัว"], pending: ["#60a5fa", "รอตรวจ"] }; const [col, lbl] = map[s] ?? ["var(--muted)", s]; return {lbl}; }; const Toast = ({ msg, type }) => { const col = type === "error" ? "var(--red)" : type === "warn" ? "var(--amber)" : "var(--green)"; return msg ? (
{msg}
) : null; }; // ── Rich text toolbar helper const inputStyle = { width: "100%", background: "var(--bg3)", border: "1px solid var(--border2)", borderRadius: 8, padding: "10px 12px", color: "var(--text)", outline: "none", transition: "border-color .2s", resize: "vertical", }; // ── Login Screen const LoginScreen = ({ onLogin }) => { const [f, setF] = useState({ url: "https://www.vanvaew.com", user: "", pass: "" }); const [loading, setLoading] = useState(false); const [err, setErr] = useState(""); const go = async () => { setLoading(true); setErr(""); try { const res = await api(f.url.replace(/\/$/, ""), "/users/me", { headers: authHead(f.user, f.pass) }); if (!res.ok) throw new Error(`HTTP ${res.status} — ตรวจสอบ username/password`); const me = await res.json(); onLogin({ ...f, url: f.url.replace(/\/$/, ""), displayName: me.name, roles: me.roles }); } catch (e) { setErr(e.message); } setLoading(false); }; return (
WP Site Manager
vanvaew.com · REST API
{[ { key: "url", lbl: "Site URL", ph: "https://www.vanvaew.com", type: "url" }, { key: "user", lbl: "Username", ph: "admin / vanvaew", type: "text" }, { key: "pass", lbl: "Application Password", ph: "xxxx xxxx xxxx xxxx xxxx xxxx", type: "password" }, ].map(({ key, lbl, ph, type }) => (
setF(p => ({ ...p, [key]: e.target.value }))} style={{ ...inputStyle, fontSize: 13 }} onFocus={e => e.target.style.borderColor = "var(--green)"} onBlur={e => e.target.style.borderColor = "var(--border2)"} onKeyDown={e => e.key === "Enter" && go()} />
))} {err &&
{err}
}
); }; // ── Main App export default function App() { const [cred, setCred] = useState(null); const [tab, setTab] = useState("posts"); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [editing, setEditing] = useState(null); // { type, item } const [toast, setToast] = useState({ msg: "", type: "ok" }); const [saving, setSaving] = useState(false); const [search, setSearch] = useState(""); const [creating, setCreating] = useState(false); const showToast = (msg, type = "ok") => { setToast({ msg, type }); setTimeout(() => setToast({ msg: "", type: "ok" }), 3500); }; // ── Fetch list const fetchList = useCallback(async (type) => { if (!cred) return; setLoading(true); setItems([]); setEditing(null); setSearch(""); try { const endpoint = type === "posts" ? "/posts?per_page=50&_fields=id,title,status,date,link" : type === "pages" ? "/pages?per_page=50&_fields=id,title,status,date,link" : ""; const res = await api(cred.url, endpoint, { headers: authHead(cred.user, cred.pass) }); if (!res.ok) throw new Error("โหลดไม่ได้"); setItems(await res.json()); } catch (e) { showToast("โหลดล้มเหลว: " + e.message, "error"); } setLoading(false); }, [cred]); useEffect(() => { if (cred && (tab === "posts" || tab === "pages")) fetchList(tab); }, [tab, cred]); // ── Open editor → fetch full content const openEdit = async (item, type) => { setEditing({ type, item: { ...item, content: { rendered: "" }, excerpt: { rendered: "" } }, loading: true }); try { const res = await api(cred.url, `/${type}/${item.id}?context=edit`, { headers: authHead(cred.user, cred.pass) }); const full = await res.json(); setEditing({ type, item: full, loading: false }); } catch (e) { showToast("โหลด content ล้มเหลว", "error"); } }; // ── Save (update) const saveEdit = async () => { if (!editing) return; setSaving(true); try { const { type, item } = editing; const body = { title: item.title?.raw ?? item.title?.rendered ?? "", content: item.content?.raw ?? item.content?.rendered ?? "", excerpt: item.excerpt?.raw ?? item.excerpt?.rendered ?? "", status: item.status, }; const res = await api(cred.url, `/${type}/${item.id}`, { method: "POST", headers: authHead(cred.user, cred.pass), body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json(); throw new Error(err.message || `HTTP ${res.status}`); } showToast(`✓ บันทึกสำเร็จ — ${item.title?.rendered || item.title?.raw || ""}`, "ok"); fetchList(type); setEditing(null); } catch (e) { showToast("บันทึกล้มเหลว: " + e.message, "error"); } setSaving(false); }; // ── Create new const createNew = async (type, data) => { setSaving(true); try { const res = await api(cred.url, `/${type}`, { method: "POST", headers: authHead(cred.user, cred.pass), body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json(); throw new Error(err.message || `HTTP ${res.status}`); } const created = await res.json(); showToast(`✓ สร้างสำเร็จ!`, "ok"); setCreating(false); fetchList(type); openEdit(created, type); } catch (e) { showToast("สร้างล้มเหลว: " + e.message, "error"); } setSaving(false); }; // ── Delete const deleteItem = async (type, id, title) => { if (!window.confirm(`⚠️ ยืนยันการลบ "${title}"?\nการกระทำนี้ไม่สามารถย้อนกลับได้`)) return; try { const res = await api(cred.url, `/${type}/${id}?force=false`, { method: "DELETE", headers: authHead(cred.user, cred.pass), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); showToast(`🗑 ย้ายไป Trash แล้ว`, "warn"); setItems(prev => prev.filter(i => i.id !== id)); if (editing?.item?.id === id) setEditing(null); } catch (e) { showToast("ลบล้มเหลว: " + e.message, "error"); } }; if (!cred) return ; const filtered = items.filter(i => { const t = (i.title?.rendered || "").toLowerCase(); return t.includes(search.toLowerCase()); }); const navItems = [ { id: "posts", icon: "📝", label: "โพสต์" }, { id: "pages", icon: "📄", label: "Pages" }, { id: "homepage", icon: "🏠", label: "Homepage" }, ]; return (
{/* ── Sidebar */}
⚡ WP Manager
{cred.url.replace(/https?:\/\//, "")}
● {cred.displayName}
{/* ── Main Content */}
{/* List panel */} {(tab === "posts" || tab === "pages") && (
{/* Toolbar */}
setSearch(e.target.value)} style={{ ...inputStyle, flex: 1, padding: "8px 12px", fontSize: 13 }} onFocus={e => e.target.style.borderColor = "var(--green)"} onBlur={e => e.target.style.borderColor = "var(--border2)"} />
{/* List */}
{loading && (
{Array(6).fill(0).map((_, i) => (
))}
)} {!loading && filtered.map(item => (
openEdit(item, tab)} style={{ padding: "12px 14px", borderRadius: 10, marginBottom: 4, background: editing?.item?.id === item.id ? "var(--bg3)" : "transparent", border: `1px solid ${editing?.item?.id === item.id ? "var(--border2)" : "transparent"}`, cursor: "pointer", transition: "all .15s", display: "flex", alignItems: "flex-start", gap: 10, }} onMouseEnter={e => { if (editing?.item?.id !== item.id) e.currentTarget.style.background = "var(--bg3)"; }} onMouseLeave={e => { if (editing?.item?.id !== item.id) e.currentTarget.style.background = "transparent"; }} >
#{item.id}
))} {!loading && filtered.length === 0 && (
{search ? "ไม่พบผลลัพธ์" : `ไม่มี ${tab === "posts" ? "โพสต์" : "Page"}`}
)}
)} {/* Editor panel */} {editing && (
{/* Editor header */}
#{editing.item.id} · {editing.type}
{editing.item.link && ( ↗ ดูบนเว็บ )}
{editing.loading ? (
กำลังโหลด...
) : (
{/* Title */}
setEditing(p => ({ ...p, item: { ...p.item, title: { ...p.item.title, raw: e.target.value } } }))} style={{ ...inputStyle, fontSize: 18, fontWeight: 700, padding: "12px 14px" }} onFocus={e => e.target.style.borderColor = "var(--green)"} onBlur={e => e.target.style.borderColor = "var(--border2)"} placeholder="ชื่อบทความ..." />
{/* Excerpt */}