| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import React, { useState, useEffect } from "react";
- import { Link } from "react-router-dom";
- import { useTheme } from "../contexts/ThemeContext";
- import { API_BASE } from "../config";
- function AdminDashboard() {
- const [posts, setPosts] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const { currentTheme, allThemes } = useTheme();
- useEffect(() => {
- fetchPosts();
- }, []);
- const fetchPosts = async () => {
- try {
- setLoading(true);
- const response = await fetch(`${API_BASE}/posts`, {
- credentials: "include",
- });
- if (!response.ok) throw new Error("Failed to fetch posts");
- const data = await response.json();
- setPosts(data);
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- };
- const deletePost = async (slug) => {
- if (!confirm(`Are you sure you want to delete this post?`)) return;
- try {
- const response = await fetch(`${API_BASE}/posts/${slug}`, {
- method: "DELETE",
- credentials: "include",
- });
- if (!response.ok) throw new Error("Failed to delete post");
- // Remove from local state
- setPosts(posts.filter((post) => post.slug !== slug));
- } catch (err) {
- setError(err.message);
- }
- };
- if (loading) {
- return (
- <div className="min-h-screen theme-bg flex items-center justify-center">
- <div className="text-center">
- <div className="animate-spin rounded-full h-12 w-12 border-b-2 theme-primary mx-auto"></div>
- <p className="mt-4 theme-text-secondary">
- Loading posts...
- </p>
- </div>
- </div>
- );
- }
- if (error) {
- return (
- <div className="min-h-screen theme-bg flex items-center justify-center">
- <div className="text-center">
- <div className="theme-surface border theme-border rounded-lg p-6">
- <h2 className="theme-text font-semibold mb-2">Error</h2>
- <p className="theme-text-secondary">{error}</p>
- <button
- onClick={fetchPosts}
- className="mt-4 btn-theme-primary text-white px-4 py-2 rounded"
- >
- Retry
- </button>
- </div>
- </div>
- </div>
- );
- }
- return (
- <div className="min-h-screen theme-bg">
- <div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
- {/* Header */}
- <div className="theme-surface shadow rounded-lg mb-6">
- <div className="px-6 py-4 border-b theme-border flex justify-between items-center">
- <div>
- <h1 className="text-2xl font-bold theme-text">
- Admin Dashboard
- </h1>
- <p className="theme-text-secondary">
- Manage your blog posts
- </p>
- </div>
- <div className="flex space-x-3">
- <Link
- to="/"
- className="btn-theme-secondary text-white px-4 py-2 rounded-lg transition-colors"
- >
- View Blog
- </Link>
- <Link
- to="/admin/themes"
- className="btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors"
- >
- Theme Manager
- </Link>
- <Link
- to="/admin/post/new"
- className="btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors"
- >
- New Post
- </Link>
- </div>
- </div>
- </div>
- {/* Stats */}
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
- <div className="theme-surface rounded-lg shadow p-6">
- <div className="flex items-center">
- <div className="p-3 rounded-full theme-bg-primary">
- <svg
- className="w-6 h-6 theme-primary"
- fill="currentColor"
- viewBox="0 0 20 20"
- >
- <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- </div>
- <div className="ml-4">
- <p className="text-sm font-medium theme-text-secondary">
- Total Posts
- </p>
- <p className="text-2xl font-bold theme-text">
- {posts.length}
- </p>
- </div>
- </div>
- </div>
- <div className="theme-surface rounded-lg shadow p-6">
- <div className="flex items-center">
- <div className="p-3 rounded-full theme-bg-primary">
- <svg
- className="w-6 h-6 theme-primary"
- fill="currentColor"
- viewBox="0 0 20 20"
- >
- <path
- fillRule="evenodd"
- 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"
- clipRule="evenodd"
- />
- </svg>
- </div>
- <div className="ml-4">
- <p className="text-sm font-medium theme-text-secondary">
- Published
- </p>
- <p className="text-2xl font-bold theme-text">
- {posts.length}
- </p>
- </div>
- </div>
- </div>
- <div className="theme-surface rounded-lg shadow p-6">
- <div className="flex items-center">
- <div className="p-3 rounded-full theme-bg-primary">
- <svg
- className="w-6 h-6 theme-primary"
- fill="currentColor"
- viewBox="0 0 20 20"
- >
- <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" />
- </svg>
- </div>
- <div className="ml-4">
- <p className="text-sm font-medium theme-text-secondary">
- Recent
- </p>
- <p className="text-2xl font-bold theme-text">
- {
- posts.filter(
- (post) =>
- new Date(post.createdAt) >
- new Date(
- Date.now() -
- 7 * 24 * 60 * 60 * 1000,
- ),
- ).length
- }
- </p>
- </div>
- </div>
- </div>
- <div className="theme-surface rounded-lg shadow p-6">
- <div className="flex items-center">
- <div className="p-3 rounded-full theme-bg-primary">
- <svg
- className="w-6 h-6 theme-primary"
- fill="currentColor"
- viewBox="0 0 20 20"
- >
- <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" />
- </svg>
- </div>
- <div className="ml-4">
- <p className="text-sm font-medium theme-text-secondary">
- Active Theme
- </p>
- <p className="text-lg font-bold theme-text">
- {currentTheme?.name || "Loading..."}
- </p>
- <p className="text-xs theme-text-secondary">
- {allThemes.length} themes available
- </p>
- </div>
- </div>
- </div>
- </div>
- {/* Posts Table */}
- <div className="theme-surface shadow rounded-lg">
- <div className="px-6 py-4 border-b theme-border">
- <h2 className="text-lg font-semibold theme-text">
- All Posts
- </h2>
- </div>
- {posts.length === 0 ? (
- <div className="px-6 py-12 text-center">
- <svg
- className="mx-auto h-12 w-12 theme-text-secondary"
- stroke="currentColor"
- fill="none"
- viewBox="0 0 48 48"
- >
- <path
- 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"
- strokeWidth={2}
- strokeLinecap="round"
- strokeLinejoin="round"
- />
- </svg>
- <h3 className="mt-2 text-sm font-medium theme-text">
- No posts
- </h3>
- <p className="mt-1 text-sm theme-text-secondary">
- Get started by creating your first post.
- </p>
- <div className="mt-6">
- <Link
- to="/admin/post/new"
- 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"
- >
- Create Post
- </Link>
- </div>
- </div>
- ) : (
- <div className="overflow-x-auto">
- <table className="min-w-full divide-y theme-border">
- <thead className="theme-bg">
- <tr>
- <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
- Title
- </th>
- <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
- Description
- </th>
- <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
- Created
- </th>
- <th className="px-6 py-3 text-left text-xs font-medium theme-text-secondary uppercase tracking-wider">
- Updated
- </th>
- <th className="px-6 py-3 text-right text-xs font-medium theme-text-secondary uppercase tracking-wider">
- Actions
- </th>
- </tr>
- </thead>
- <tbody className="theme-surface divide-y theme-border">
- {posts.map((post) => (
- <tr
- key={post.slug}
- className="hover:theme-bg"
- >
- <td className="px-6 py-4 whitespace-nowrap">
- <div className="text-sm font-medium theme-text">
- {post.title}
- </div>
- <div className="text-sm theme-text-secondary">
- {post.slug}
- </div>
- </td>
- <td className="px-6 py-4">
- <div className="text-sm theme-text max-w-xs truncate">
- {post.description}
- </div>
- </td>
- <td className="px-6 py-4 whitespace-nowrap text-sm theme-text-secondary">
- {new Date(
- post.createdAt,
- ).toLocaleDateString()}
- </td>
- <td className="px-6 py-4 whitespace-nowrap text-sm theme-text-secondary">
- {new Date(
- post.updatedAt,
- ).toLocaleDateString()}
- </td>
- <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
- <div className="flex justify-end space-x-2">
- <Link
- to={`/posts/${post.slug}`}
- className="theme-primary"
- >
- View
- </Link>
- <Link
- to={`/admin/post/${post.slug}/edit`}
- className="theme-secondary"
- >
- Edit
- </Link>
- <button
- onClick={() =>
- deletePost(
- post.slug,
- )
- }
- className="theme-accent"
- >
- Delete
- </button>
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- )}
- </div>
- </div>
- </div>
- );
- }
- export default AdminDashboard;
|