AdminDashboard.jsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import React, { useState, useEffect } from "react";
  2. import { Link } from "react-router-dom";
  3. import { useTheme } from "../contexts/ThemeContext";
  4. import { API_BASE } from "../config";
  5. function AdminDashboard() {
  6. const [posts, setPosts] = useState([]);
  7. const [loading, setLoading] = useState(true);
  8. const [error, setError] = useState(null);
  9. const { currentTheme, allThemes } = useTheme();
  10. useEffect(() => {
  11. fetchPosts();
  12. }, []);
  13. const fetchPosts = async () => {
  14. try {
  15. setLoading(true);
  16. const response = await fetch(`${API_BASE}/posts`, {
  17. credentials: "include",
  18. });
  19. if (!response.ok) throw new Error("Failed to fetch posts");
  20. const data = await response.json();
  21. setPosts(data);
  22. } catch (err) {
  23. setError(err.message);
  24. } finally {
  25. setLoading(false);
  26. }
  27. };
  28. const deletePost = async (slug) => {
  29. if (!confirm(`Are you sure you want to delete this post?`)) return;
  30. try {
  31. const response = await fetch(`${API_BASE}/posts/${slug}`, {
  32. method: "DELETE",
  33. credentials: "include",
  34. });
  35. if (!response.ok) throw new Error("Failed to delete post");
  36. // Remove from local state
  37. setPosts(posts.filter((post) => post.slug !== slug));
  38. } catch (err) {
  39. setError(err.message);
  40. }
  41. };
  42. if (loading) {
  43. return (
  44. <div className="min-h-screen theme-bg flex items-center justify-center">
  45. <div className="text-center">
  46. <div className="animate-spin rounded-full h-12 w-12 border-b-2 theme-primary mx-auto"></div>
  47. <p className="mt-4 theme-text-secondary">
  48. Loading posts...
  49. </p>
  50. </div>
  51. </div>
  52. );
  53. }
  54. if (error) {
  55. return (
  56. <div className="min-h-screen theme-bg flex items-center justify-center">
  57. <div className="text-center">
  58. <div className="theme-surface border theme-border rounded-lg p-6">
  59. <h2 className="theme-text font-semibold mb-2">Error</h2>
  60. <p className="theme-text-secondary">{error}</p>
  61. <button
  62. onClick={fetchPosts}
  63. className="mt-4 btn-theme-primary text-white px-4 py-2 rounded"
  64. >
  65. Retry
  66. </button>
  67. </div>
  68. </div>
  69. </div>
  70. );
  71. }
  72. return (
  73. <div className="min-h-screen theme-bg">
  74. <div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
  75. {/* Header */}
  76. <div className="theme-surface shadow rounded-lg mb-6">
  77. <div className="px-6 py-4 border-b theme-border flex justify-between items-center">
  78. <div>
  79. <h1 className="text-2xl font-bold theme-text">
  80. Admin Dashboard
  81. </h1>
  82. <p className="theme-text-secondary">
  83. Manage your blog posts
  84. </p>
  85. </div>
  86. <div className="flex space-x-3">
  87. <Link
  88. to="/"
  89. className="btn-theme-secondary text-white px-4 py-2 rounded-lg transition-colors"
  90. >
  91. View Blog
  92. </Link>
  93. <Link
  94. to="/admin/themes"
  95. className="btn-theme-secondary text-white px-4 py-2 rounded-lg transition-colors"
  96. >
  97. Themes
  98. </Link>
  99. <Link
  100. to="/admin/media"
  101. className="btn-theme-secondary text-white px-4 py-2 rounded-lg transition-colors"
  102. >
  103. Media
  104. </Link>
  105. <Link
  106. to="/admin/post/new"
  107. className="btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors"
  108. >
  109. New Post
  110. </Link>
  111. </div>
  112. </div>
  113. </div>
  114. {/* Stats */}
  115. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
  116. <div className="theme-surface rounded-lg shadow p-6">
  117. <div className="flex items-center">
  118. <div className="p-3 rounded-full theme-bg-primary">
  119. <svg
  120. className="w-6 h-6 theme-primary"
  121. fill="currentColor"
  122. viewBox="0 0 20 20"
  123. >
  124. <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
  125. </svg>
  126. </div>
  127. <div className="ml-4">
  128. <p className="text-sm font-medium theme-text-secondary">
  129. Total Posts
  130. </p>
  131. <p className="text-2xl font-bold theme-text">
  132. {posts.length}
  133. </p>
  134. </div>
  135. </div>
  136. </div>
  137. <div className="theme-surface rounded-lg shadow p-6">
  138. <div className="flex items-center">
  139. <div className="p-3 rounded-full theme-bg-primary">
  140. <svg
  141. className="w-6 h-6 theme-primary"
  142. fill="currentColor"
  143. viewBox="0 0 20 20"
  144. >
  145. <path
  146. fillRule="evenodd"
  147. d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
  148. clipRule="evenodd"
  149. />
  150. </svg>
  151. </div>
  152. <div className="ml-4">
  153. <p className="text-sm font-medium theme-text-secondary">
  154. Published
  155. </p>
  156. <p className="text-2xl font-bold theme-text">
  157. {posts.length}
  158. </p>
  159. </div>
  160. </div>
  161. </div>
  162. <div className="theme-surface rounded-lg shadow p-6">
  163. <div className="flex items-center">
  164. <div className="p-3 rounded-full theme-bg-primary">
  165. <svg
  166. className="w-6 h-6 theme-primary"
  167. fill="currentColor"
  168. viewBox="0 0 20 20"
  169. >
  170. <path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3z" />
  171. </svg>
  172. </div>
  173. <div className="ml-4">
  174. <p className="text-sm font-medium theme-text-secondary">
  175. Recent
  176. </p>
  177. <p className="text-2xl font-bold theme-text">
  178. {
  179. posts.filter(
  180. (post) =>
  181. new Date(post.createdAt) >
  182. new Date(
  183. Date.now() -
  184. 7 * 24 * 60 * 60 * 1000,
  185. ),
  186. ).length
  187. }
  188. </p>
  189. </div>
  190. </div>
  191. </div>
  192. <div className="theme-surface rounded-lg shadow p-6">
  193. <div className="flex items-center">
  194. <div className="p-3 rounded-full theme-bg-primary">
  195. <svg
  196. className="w-6 h-6 theme-primary"
  197. fill="currentColor"
  198. viewBox="0 0 20 20"
  199. >
  200. <path d="M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a.402.402 0 01-.4.402l-1.13.043a.402.402 0 01-.426-.402H4a1 1 0 110-2h3V3a1 1 0 011-1zM4 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 000-2H4zM10 9a1 1 0 100 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 100-2h-1z" />
  201. </svg>
  202. </div>
  203. <div className="ml-4">
  204. <p className="text-sm font-medium theme-text-secondary">
  205. Active Theme
  206. </p>
  207. <p className="text-lg font-bold theme-text">
  208. {currentTheme?.name || "Loading..."}
  209. </p>
  210. <p className="text-xs theme-text-secondary">
  211. {allThemes.length} themes available
  212. </p>
  213. </div>
  214. </div>
  215. </div>
  216. </div>
  217. {/* Posts Table */}
  218. <div className="theme-surface shadow rounded-lg">
  219. <div className="px-6 py-4 border-b theme-border">
  220. <h2 className="text-lg font-semibold theme-text">
  221. All Posts
  222. </h2>
  223. </div>
  224. {posts.length === 0 ? (
  225. <div className="px-6 py-12 text-center">
  226. <svg
  227. className="mx-auto h-12 w-12 theme-text-secondary"
  228. stroke="currentColor"
  229. fill="none"
  230. viewBox="0 0 48 48"
  231. >
  232. <path
  233. d="M34 40h10v-4a6 6 0 00-10.712-3.714M34 40H14m20 0v-4a9.971 9.971 0 00-.712-3.714M14 40H4v-4a6 6 0 0110.713-3.714M14 40v-4c0-1.313.253-2.566.713-3.714m0 0A9.971 9.971 0 0118 28a9.971 9.971 0 014 4.286"
  234. strokeWidth={2}
  235. strokeLinecap="round"
  236. strokeLinejoin="round"
  237. />
  238. </svg>
  239. <h3 className="mt-2 text-sm font-medium theme-text">
  240. No posts
  241. </h3>
  242. <p className="mt-1 text-sm theme-text-secondary">
  243. Get started by creating your first post.
  244. </p>
  245. <div className="mt-6">
  246. <Link
  247. to="/admin/post/new"
  248. className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white btn-theme-primary"
  249. >
  250. Create Post
  251. </Link>
  252. </div>
  253. </div>
  254. ) : (
  255. <div className="overflow-x-auto">
  256. <table className="min-w-full divide-y theme-border">
  257. <thead className="theme-bg">
  258. <tr>
  259. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  260. Title
  261. </th>
  262. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  263. Description
  264. </th>
  265. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  266. Created
  267. </th>
  268. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  269. Updated
  270. </th>
  271. <th className="px-6 py-3 text-right text-xs font-medium theme-text-secondary uppercase tracking-wider">
  272. Actions
  273. </th>
  274. </tr>
  275. </thead>
  276. <tbody className="theme-surface divide-y theme-border">
  277. {posts.map((post) => (
  278. <tr
  279. key={post.slug}
  280. className="hover:theme-bg"
  281. >
  282. <td className="px-6 py-4 whitespace-nowrap">
  283. <div className="text-sm font-medium theme-text">
  284. {post.title}
  285. </div>
  286. <div className="text-sm theme-text-secondary">
  287. {post.slug}
  288. </div>
  289. </td>
  290. <td className="px-6 py-4">
  291. <div className="text-sm theme-text max-w-xs truncate">
  292. {post.description}
  293. </div>
  294. </td>
  295. <td className="px-6 py-4 whitespace-nowrap text-sm theme-text-secondary">
  296. {new Date(
  297. post.createdAt,
  298. ).toLocaleDateString()}
  299. </td>
  300. <td className="px-6 py-4 whitespace-nowrap text-sm theme-text-secondary">
  301. {new Date(
  302. post.updatedAt,
  303. ).toLocaleDateString()}
  304. </td>
  305. <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
  306. <div className="flex justify-end space-x-2">
  307. <Link
  308. to={`/posts/${post.slug}`}
  309. className="theme-primary"
  310. >
  311. View
  312. </Link>
  313. <Link
  314. to={`/admin/post/${post.slug}/edit`}
  315. className="theme-secondary"
  316. >
  317. Edit
  318. </Link>
  319. <button
  320. onClick={() =>
  321. deletePost(
  322. post.slug,
  323. )
  324. }
  325. className="theme-accent"
  326. >
  327. Delete
  328. </button>
  329. </div>
  330. </td>
  331. </tr>
  332. ))}
  333. </tbody>
  334. </table>
  335. </div>
  336. )}
  337. </div>
  338. </div>
  339. </div>
  340. );
  341. }
  342. export default AdminDashboard;