AdminDashboard.jsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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-primary text-white px-4 py-2 rounded-lg transition-colors"
  96. >
  97. Theme Manager
  98. </Link>
  99. <Link
  100. to="/admin/post/new"
  101. className="btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors"
  102. >
  103. New Post
  104. </Link>
  105. </div>
  106. </div>
  107. </div>
  108. {/* Stats */}
  109. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
  110. <div className="theme-surface rounded-lg shadow p-6">
  111. <div className="flex items-center">
  112. <div className="p-3 rounded-full theme-bg-primary">
  113. <svg
  114. className="w-6 h-6 theme-primary"
  115. fill="currentColor"
  116. viewBox="0 0 20 20"
  117. >
  118. <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
  119. </svg>
  120. </div>
  121. <div className="ml-4">
  122. <p className="text-sm font-medium theme-text-secondary">
  123. Total Posts
  124. </p>
  125. <p className="text-2xl font-bold theme-text">
  126. {posts.length}
  127. </p>
  128. </div>
  129. </div>
  130. </div>
  131. <div className="theme-surface rounded-lg shadow p-6">
  132. <div className="flex items-center">
  133. <div className="p-3 rounded-full theme-bg-primary">
  134. <svg
  135. className="w-6 h-6 theme-primary"
  136. fill="currentColor"
  137. viewBox="0 0 20 20"
  138. >
  139. <path
  140. fillRule="evenodd"
  141. 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"
  142. clipRule="evenodd"
  143. />
  144. </svg>
  145. </div>
  146. <div className="ml-4">
  147. <p className="text-sm font-medium theme-text-secondary">
  148. Published
  149. </p>
  150. <p className="text-2xl font-bold theme-text">
  151. {posts.length}
  152. </p>
  153. </div>
  154. </div>
  155. </div>
  156. <div className="theme-surface rounded-lg shadow p-6">
  157. <div className="flex items-center">
  158. <div className="p-3 rounded-full theme-bg-primary">
  159. <svg
  160. className="w-6 h-6 theme-primary"
  161. fill="currentColor"
  162. viewBox="0 0 20 20"
  163. >
  164. <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" />
  165. </svg>
  166. </div>
  167. <div className="ml-4">
  168. <p className="text-sm font-medium theme-text-secondary">
  169. Recent
  170. </p>
  171. <p className="text-2xl font-bold theme-text">
  172. {
  173. posts.filter(
  174. (post) =>
  175. new Date(post.createdAt) >
  176. new Date(
  177. Date.now() -
  178. 7 * 24 * 60 * 60 * 1000,
  179. ),
  180. ).length
  181. }
  182. </p>
  183. </div>
  184. </div>
  185. </div>
  186. <div className="theme-surface rounded-lg shadow p-6">
  187. <div className="flex items-center">
  188. <div className="p-3 rounded-full theme-bg-primary">
  189. <svg
  190. className="w-6 h-6 theme-primary"
  191. fill="currentColor"
  192. viewBox="0 0 20 20"
  193. >
  194. <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" />
  195. </svg>
  196. </div>
  197. <div className="ml-4">
  198. <p className="text-sm font-medium theme-text-secondary">
  199. Active Theme
  200. </p>
  201. <p className="text-lg font-bold theme-text">
  202. {currentTheme?.name || "Loading..."}
  203. </p>
  204. <p className="text-xs theme-text-secondary">
  205. {allThemes.length} themes available
  206. </p>
  207. </div>
  208. </div>
  209. </div>
  210. </div>
  211. {/* Posts Table */}
  212. <div className="theme-surface shadow rounded-lg">
  213. <div className="px-6 py-4 border-b theme-border">
  214. <h2 className="text-lg font-semibold theme-text">
  215. All Posts
  216. </h2>
  217. </div>
  218. {posts.length === 0 ? (
  219. <div className="px-6 py-12 text-center">
  220. <svg
  221. className="mx-auto h-12 w-12 theme-text-secondary"
  222. stroke="currentColor"
  223. fill="none"
  224. viewBox="0 0 48 48"
  225. >
  226. <path
  227. 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"
  228. strokeWidth={2}
  229. strokeLinecap="round"
  230. strokeLinejoin="round"
  231. />
  232. </svg>
  233. <h3 className="mt-2 text-sm font-medium theme-text">
  234. No posts
  235. </h3>
  236. <p className="mt-1 text-sm theme-text-secondary">
  237. Get started by creating your first post.
  238. </p>
  239. <div className="mt-6">
  240. <Link
  241. to="/admin/post/new"
  242. 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"
  243. >
  244. Create Post
  245. </Link>
  246. </div>
  247. </div>
  248. ) : (
  249. <div className="overflow-x-auto">
  250. <table className="min-w-full divide-y theme-border">
  251. <thead className="theme-bg">
  252. <tr>
  253. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  254. Title
  255. </th>
  256. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  257. Description
  258. </th>
  259. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  260. Created
  261. </th>
  262. <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
  263. Updated
  264. </th>
  265. <th className="px-6 py-3 text-right text-xs font-medium theme-text-secondary uppercase tracking-wider">
  266. Actions
  267. </th>
  268. </tr>
  269. </thead>
  270. <tbody className="theme-surface divide-y theme-border">
  271. {posts.map((post) => (
  272. <tr
  273. key={post.slug}
  274. className="hover:theme-bg"
  275. >
  276. <td className="px-6 py-4 whitespace-nowrap">
  277. <div className="text-sm font-medium theme-text">
  278. {post.title}
  279. </div>
  280. <div className="text-sm theme-text-secondary">
  281. {post.slug}
  282. </div>
  283. </td>
  284. <td className="px-6 py-4">
  285. <div className="text-sm theme-text max-w-xs truncate">
  286. {post.description}
  287. </div>
  288. </td>
  289. <td className="px-6 py-4 whitespace-nowrap text-sm theme-text-secondary">
  290. {new Date(
  291. post.createdAt,
  292. ).toLocaleDateString()}
  293. </td>
  294. <td className="px-6 py-4 whitespace-nowrap text-sm theme-text-secondary">
  295. {new Date(
  296. post.updatedAt,
  297. ).toLocaleDateString()}
  298. </td>
  299. <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
  300. <div className="flex justify-end space-x-2">
  301. <Link
  302. to={`/posts/${post.slug}`}
  303. className="theme-primary"
  304. >
  305. View
  306. </Link>
  307. <Link
  308. to={`/admin/post/${post.slug}/edit`}
  309. className="theme-secondary"
  310. >
  311. Edit
  312. </Link>
  313. <button
  314. onClick={() =>
  315. deletePost(
  316. post.slug,
  317. )
  318. }
  319. className="theme-accent"
  320. >
  321. Delete
  322. </button>
  323. </div>
  324. </td>
  325. </tr>
  326. ))}
  327. </tbody>
  328. </table>
  329. </div>
  330. )}
  331. </div>
  332. </div>
  333. </div>
  334. );
  335. }
  336. export default AdminDashboard;