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 }) => (
{lbl}
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}
}
{loading ? <> กำลังเชื่อมต่อ...> : "→ เข้าสู่ระบบ"}
);
};
// ── 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}
{navItems.map(n => (
setTab(n.id)}
style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "9px 12px", borderRadius: 8, border: "none", background: tab === n.id ? "var(--green-bg)" : "transparent", color: tab === n.id ? "var(--green)" : "var(--muted)", fontSize: 13, fontWeight: tab === n.id ? 600 : 400, textAlign: "left", transition: "all .15s", marginBottom: 2 }}>
{n.icon} {n.label}
))}
{ setCred(null); setItems([]); setEditing(null); }}
style={{ width: "100%", padding: "8px 12px", borderRadius: 8, border: "none", background: "transparent", color: "var(--sub)", fontSize: 12, textAlign: "left", fontFamily: "'Sarabun'" }}>
← ออกจากระบบ
{/* ── 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)"}
/>
setCreating(true)}
style={{ flexShrink: 0, padding: "8px 14px", background: "var(--green)", border: "none", borderRadius: 8, color: "#000", fontWeight: 700, fontSize: 13, whiteSpace: "nowrap" }}>
+ สร้างใหม่
fetchList(tab)}
style={{ flexShrink: 0, padding: "8px 10px", background: "var(--bg3)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--muted)", fontSize: 13 }}>
↻
{/* 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"; }}
>
{ e.stopPropagation(); deleteItem(tab, item.id, item.title?.rendered || ""); }}
style={{ flexShrink: 0, padding: "4px 8px", background: "transparent", border: "none", color: "var(--sub)", fontSize: 14, borderRadius: 6, transition: "color .15s" }}
onMouseEnter={e => e.currentTarget.style.color = "var(--red)"}
onMouseLeave={e => e.currentTarget.style.color = "var(--sub)"}
title="ย้ายไป Trash"
>🗑
))}
{!loading && filtered.length === 0 && (
{search ? "ไม่พบผลลัพธ์" : `ไม่มี ${tab === "posts" ? "โพสต์" : "Page"}`}
)}
)}
{/* Editor panel */}
{editing && (
{/* Editor header */}
setEditing(null)}
style={{ padding: "6px 10px", background: "var(--bg3)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--muted)", fontSize: 13 }}>
← กลับ
#{editing.item.id} · {editing.type}
{editing.item.link && (
↗ ดูบนเว็บ
)}
setEditing(p => ({ ...p, item: { ...p.item, status: e.target.value } }))}
style={{ padding: "6px 10px", background: "var(--bg3)", border: "1px solid var(--border2)", borderRadius: 8, color: "var(--text)", fontSize: 13 }}>
เผยแพร่
ร่าง
ส่วนตัว
รอตรวจ
{saving ? <> กำลังบันทึก...> : "💾 บันทึก"}
{editing.loading ? (
กำลังโหลด...
) : (
{/* Title */}
ชื่อเรื่อง (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 */}
สรุปย่อ (Excerpt)
{/* Content */}
เนื้อหา (Content HTML)
{(editing.item.content?.raw ?? "").length} ตัวอักษร
)}
)}
{/* Homepage Tab */}
{tab === "homepage" &&
}
{/* Empty state */}
{(tab === "posts" || tab === "pages") && !editing && !loading && items.length > 0 && (
👆
เลือก{tab === "posts" ? "โพสต์" : "Page"}ที่ต้องการแก้ไข
)}
{/* Create Modal */}
{creating &&
setCreating(false)} onCreate={createNew} saving={saving} />}
);
}
// ── Homepage Editor
function HomepageEditor({ cred, showToast }) {
const [pages, setPages] = useState([]);
const [hp, setHp] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
(async () => {
setLoading(true);
try {
// Get reading settings to find homepage ID
const settRes = await fetch(`${cred.url}/wp-json`, { headers: authHead(cred.user, cred.pass) });
// Fallback: get front page via pages list
const pRes = await api(cred.url, "/pages?per_page=50&_fields=id,title,status", { headers: authHead(cred.user, cred.pass) });
const ps = await pRes.json();
setPages(ps);
// Try to get the settings
const optRes = await fetch(`${cred.url}/wp-json/wp/v2/settings`, { headers: authHead(cred.user, cred.pass) });
if (optRes.ok) {
const opts = await optRes.json();
const frontId = opts.page_on_front;
if (frontId) {
const fpRes = await api(cred.url, `/pages/${frontId}?context=edit`, { headers: authHead(cred.user, cred.pass) });
if (fpRes.ok) { setHp(await fpRes.json()); }
}
}
} catch (e) { }
setLoading(false);
})();
}, [cred]);
const loadPage = async (id) => {
setLoading(true);
const res = await api(cred.url, `/pages/${id}?context=edit`, { headers: authHead(cred.user, cred.pass) });
if (res.ok) setHp(await res.json());
setLoading(false);
};
const save = async () => {
if (!hp) return;
setSaving(true);
try {
const res = await api(cred.url, `/pages/${hp.id}`, {
method: "POST",
headers: authHead(cred.user, cred.pass),
body: JSON.stringify({
title: hp.title?.raw ?? hp.title?.rendered,
content: hp.content?.raw ?? hp.content?.rendered,
status: hp.status,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
showToast("✓ บันทึก Homepage สำเร็จ!", "ok");
} catch (e) { showToast("บันทึกล้มเหลว: " + e.message, "error"); }
setSaving(false);
};
return (
🏠 แก้ไข Homepage
loadPage(e.target.value)} style={{ padding: "6px 10px", background: "var(--bg3)", border: "1px solid var(--border2)", borderRadius: 8, color: "var(--text)", fontSize: 13 }}>
— เลือก Page —
{pages.map(p => {p.title?.rendered || `#${p.id}`} )}
{hp && (
{saving ? <> บันทึก...> : "💾 บันทึก"}
)}
{loading ? (
กำลังโหลด...
) : !hp ? (
🏠
เลือก Page ที่เป็น Homepage จาก dropdown ด้านบน
ปกติคือ Page ชื่อ "Home" หรือ "หน้าแรก"
) : (
)}
);
}
// ── Create Modal
function CreateModal({ type, onClose, onCreate, saving }) {
const [f, setF] = useState({ title: "", content: "", status: "draft" });
const lbl = type === "posts" ? "โพสต์ใหม่" : "Page ใหม่";
return (
+ สร้าง{lbl}
ชื่อเรื่อง
setF(p => ({ ...p, title: e.target.value }))}
style={{ ...inputStyle, fontSize: 15 }}
onFocus={e => e.target.style.borderColor = "var(--green)"}
onBlur={e => e.target.style.borderColor = "var(--border2)"}
placeholder={type === "posts" ? "ชื่อบทความ..." : "ชื่อหน้า..."}
autoFocus
/>
สถานะ
setF(p => ({ ...p, status: e.target.value }))}
style={{ ...inputStyle, height: "auto", padding: "9px 12px" }}>
ร่าง (draft)
เผยแพร่ทันที
ยกเลิก
onCreate(type, f)} disabled={saving || !f.title}
style={{ padding: "9px 20px", background: f.title ? "var(--green)" : "var(--bg3)", border: "none", borderRadius: 8, color: f.title ? "#000" : "var(--muted)", fontWeight: 700, fontSize: 14, display: "flex", alignItems: "center", gap: 6 }}>
{saving ? <> กำลังสร้าง...> : `+ สร้าง${lbl}`}
);
}All 404 Redirect to Homepage has detected that the target URL is invalid. This will cause an infinite loop redirection. Please go to the plugin settings and correct the target link!