All Articles
Article related with how blogs get uploads

Article related with how blogs get uploads

Admin·July 8, 2026·42 min read
import React, { useEffect, useState, useRef } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { fetchCategoriesTree } from '../../lib/db';
import { CategoryWithChildren } from '../../types';
import {
  Blog, BlogListItem, fetchBlogsList, fetchBlogById,
  createBlog, updateBlog, deleteBlog, checkSlugExists,
} from '../../lib/blogDb';
import { uploadToCloudinary } from '../../lib/cloudinaryConfig';
import {
  Plus, Edit2, Trash2, Eye, EyeOff, Save, ArrowLeft,
  Image as ImageIcon, Video, Paperclip, Tag, Search,
  Calendar, FileText, Globe, Bold, Italic, Underline,
  List, ListOrdered, Quote, Link2, Code, Heading1,
  Heading2, Heading3, Loader2, ChevronLeft, ChevronRight,
  X, Strikethrough, ExternalLink,
} from 'lucide-react';
import { toast } from 'sonner';
import Modal from '../../components/ui/Modal';

const inputCls =
  'w-full px-4 py-2.5 rounded-xl border border-gray-200 dark:border-white/[0.08] ' +
  'bg-white dark:bg-[#111d35] text-gray-900 dark:text-white ' +
  'placeholder-gray-400 dark:placeholder-gray-500 ' +
  'focus:outline-none focus:ring-2 focus:ring-violet-500/60 transition text-sm';
const labelCls = 'block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5';

const PAGE_SIZE = 10;

const toSlug = (title: string) =>
  title.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');

const estimateReadTime = (html: string) => {
  const text = html.replace(/<[^>]*>/g, ' ');
  const words = text.split(/\s+/).filter(Boolean).length;
  return Math.max(1, Math.ceil(words / 200));
};

type View = 'list' | 'editor';

const ManageBlogs: React.FC = () => {
  const { user } = useAuth();
  const [view, setView]           = useState<View>('list');
  const [blogs, setBlogs]         = useState<BlogListItem[]>([]);
  const [total, setTotal]         = useState(0);
  const [page, setPage]           = useState(0);
  const [loading, setLoading]     = useState(true);
  const [saving, setSaving]       = useState(false);
  const [search, setSearch]       = useState('');
  const [categoryTree, setCategoryTree] = useState<CategoryWithChildren[]>([]);
  const [deleteId, setDeleteId]   = useState<string | null>(null);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [previewBlog, setPreviewBlog] = useState<Blog | null>(null);
  const [previewLoading, setPreviewLoading] = useState(false);

  const [form, setForm] = useState({
    title: '', slug: '', excerpt: '',
    cover_image_url: '', video_url: '',
    attachment_url: '', attachment_name: '',
    meta_title: '', meta_description: '',
    tags: '', category_id: '', subcategory_id: '',
  });
  const [slugManual, setSlugManual] = useState(false);
  const editorRef = useRef<HTMLDivElement>(null);
  const [contentHtml, setContentHtml] = useState('');

  const [uploadingCover, setUploadingCover]             = useState(false);
  const [uploadingVideo, setUploadingVideo]             = useState(false);
  const [uploadingAttachment, setUploadingAttachment]   = useState(false);
  const [uploadingInlineImage, setUploadingInlineImage] = useState(false);

  const coverInputRef       = useRef<HTMLInputElement>(null);
  const videoInputRef       = useRef<HTMLInputElement>(null);
  const attachmentInputRef  = useRef<HTMLInputElement>(null);
  const inlineImageInputRef = useRef<HTMLInputElement>(null);

  useEffect(() => { loadList(); loadCategories(); }, [page]);

  const loadList = async () => {
    setLoading(true);
    const { blogs: data, total: count } = await fetchBlogsList(page, PAGE_SIZE, {
      search: search || undefined,
    });
    setBlogs(data);
    setTotal(count);
    setLoading(false);
  };

  const loadCategories = async () => {
    const tree = await fetchCategoriesTree();
    setCategoryTree(tree);
  };

  const handleSearch = () => { setPage(0); loadList(); };

  const openNew = () => {
    setEditingId(null);
    setSlugManual(false);
    setForm({
      title: '', slug: '', excerpt: '',
      cover_image_url: '', video_url: '',
      attachment_url: '', attachment_name: '',
      meta_title: '', meta_description: '',
      tags: '', category_id: '', subcategory_id: '',
    });
    setContentHtml('');
    setView('editor');
    setTimeout(() => { if (editorRef.current) editorRef.current.innerHTML = ''; }, 0);
  };

  const openEdit = async (id: string) => {
    setLoading(true);
    const blog = await fetchBlogById(id);
    setLoading(false);
    if (!blog) { toast.error('Failed to load blog'); return; }

    setEditingId(blog.id);
    setSlugManual(true);
    setForm({
      title: blog.title,
      slug: blog.slug,
      excerpt: blog.excerpt || '',
      cover_image_url: blog.cover_image_url || '',
      video_url: blog.video_url || '',
      attachment_url: blog.attachment_url || '',
      attachment_name: blog.attachment_name || '',
      meta_title: blog.meta_title || '',
      meta_description: blog.meta_description || '',
      tags: (blog.tags || []).join(', '),
      category_id: blog.category_id || '',
      subcategory_id: blog.subcategory_id || '',
    });
    setContentHtml(blog.content || '');
    setView('editor');
    setTimeout(() => {
      if (editorRef.current) editorRef.current.innerHTML = blog.content || '';
    }, 0);
  };

  const openPreview = async (id: string) => {
    setPreviewLoading(true);
    const blog = await fetchBlogById(id);
    setPreviewLoading(false);
    if (!blog) { toast.error('Failed to load preview'); return; }
    setPreviewBlog(blog);
  };

  const handleTitleChange = (title: string) => {
    setForm((p) => ({
      ...p,
      title,
      slug: slugManual ? p.slug : toSlug(title),
      meta_title: p.meta_title || title,
    }));
  };

  const subsForParent = (parentId: string) =>
    categoryTree.find((c) => c.id === parentId)?.children || [];

  const exec = (cmd: string, value?: string) => {
    document.execCommand(cmd, false, value);
    editorRef.current?.focus();
    setContentHtml(editorRef.current?.innerHTML || '');
  };

  const insertLink = () => {
    const url = prompt('Enter URL:');
    if (url) exec('createLink', url);
  };

  const handleEditorInput = () => {
    setContentHtml(editorRef.current?.innerHTML || '');
  };

  const handleCoverUpload = async (file: File) => {
    setUploadingCover(true);
    const result = await uploadToCloudinary(file, 'image');
    setUploadingCover(false);
    if (result) { setForm((p) => ({ ...p, cover_image_url: result.url })); toast.success('Cover image uploaded!'); }
    else toast.error('Upload failed — check Cloudinary preset config');
  };

  const handleVideoUpload = async (file: File) => {
    setUploadingVideo(true);
    const result = await uploadToCloudinary(file, 'video');
    setUploadingVideo(false);
    if (result) { setForm((p) => ({ ...p, video_url: result.url })); toast.success('Video uploaded!'); }
    else toast.error('Upload failed — check Cloudinary preset config');
  };

  const handleAttachmentUpload = async (file: File) => {
    setUploadingAttachment(true);
    const result = await uploadToCloudinary(file, 'raw');
    setUploadingAttachment(false);
    if (result) {
      setForm((p) => ({ ...p, attachment_url: result.url, attachment_name: result.name }));
      toast.success('File uploaded!');
    } else toast.error('Upload failed — check Cloudinary preset config');
  };

  const handleInlineImageUpload = async (file: File) => {
    setUploadingInlineImage(true);
    const result = await uploadToCloudinary(file, 'image');
    setUploadingInlineImage(false);
    if (result) {
      exec('insertImage', result.url);
      toast.success('Image inserted!');
    } else toast.error('Upload failed — check Cloudinary preset config');
  };

  const handleSave = async (publish: boolean) => {
    if (!form.title.trim())   { toast.error('Title is required'); return; }
    if (!form.slug.trim())    { toast.error('Slug is required'); return; }
    if (!contentHtml.trim() || contentHtml === '<br>') { toast.error('Content is required'); return; }

    const slugTaken = await checkSlugExists(form.slug.trim(), editingId || undefined);
    if (slugTaken) { toast.error('This slug is already used by another post'); return; }

    setSaving(true);
    const textOnly = contentHtml.replace(/<[^>]*>/g, ' ').trim();
    const payload = {
      title: form.title.trim(),
      slug: form.slug.trim(),
      content: contentHtml,
      excerpt: form.excerpt.trim() || textOnly.slice(0, 200) + '…',
      cover_image_url: form.cover_image_url.trim() || null,
      video_url: form.video_url.trim() || null,
      attachment_url: form.attachment_url.trim() || null,
      attachment_name: form.attachment_name.trim() || null,
      meta_title: form.meta_title.trim() || form.title.trim(),
      meta_description: form.meta_description.trim() || '',
      tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean),
      category_id: form.category_id || null,
      subcategory_id: form.subcategory_id || null,
      is_published: publish,
      published_at: publish ? new Date().toISOString() : null,
      author_name: user?.name || 'Admin',
      read_time_minutes: estimateReadTime(contentHtml),
    };

    let result;
    if (editingId) result = await updateBlog(editingId, payload);
    else            result = await createBlog(payload as any);

    setSaving(false);
    if (result) {
      toast.success(publish ? 'Blog published!' : 'Saved as draft');
      setView('list');
      loadList();
    } else {
      toast.error('Failed to save blog');
    }
  };

  const handleDelete = async () => {
    if (!deleteId) return;
    const ok = await deleteBlog(deleteId);
    if (ok) { toast.success('Blog deleted'); setDeleteId(null); loadList(); }
    else toast.error('Failed to delete');
  };

  const handleTogglePublish = async (blog: BlogListItem) => {
    const updated = await updateBlog(blog.id, {
      is_published: !blog.is_published,
      published_at: !blog.is_published ? new Date().toISOString() : null,
    });
    if (updated) { toast.success(blog.is_published ? 'Unpublished' : 'Published!'); loadList(); }
    else toast.error('Failed to update');
  };

  const totalPages = Math.ceil(total / PAGE_SIZE);

  const ToolBtn: React.FC<{ icon: React.ReactNode; onClick: () => void; title: string }> = ({ icon, onClick, title }) => (
    <button
      type="button"
      onClick={onClick}
      title={title}
      className="p-2 rounded-lg text-gray-600 dark:text-gray-300
        hover:bg-gray-100 dark:hover:bg-white/[0.08]
        border border-transparent hover:border-gray-200 dark:hover:border-white/[0.08] transition"
    >
      {icon}
    </button>
  );

  if (view === 'editor') {
    return (
      <div className="space-y-6">
        <div className="flex items-center justify-between gap-4 flex-wrap">
          <div className="flex items-center gap-3">
            <button
              onClick={() => setView('list')}
              className="p-2 rounded-xl border border-gray-200 dark:border-white/[0.08]
                text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/[0.05] transition"
            >
              <ArrowLeft className="w-4 h-4" />
            </button>
            <div>
              <h1 className="text-xl font-bold text-gray-800 dark:text-white">
                {editingId ? 'Edit Article' : 'New Article'}
              </h1>
              {contentHtml && (
                <p className="text-xs text-gray-400 mt-0.5">~{estimateReadTime(contentHtml)} min read</p>
              )}
            </div>
          </div>
          <div className="flex items-center gap-2">
            <button
              onClick={() => setPreviewBlog({
                id: editingId || 'preview',
                title: form.title || 'Untitled',
                slug: form.slug || 'preview',
                content: contentHtml,
                excerpt: form.excerpt,
                cover_image_url: form.cover_image_url || null,
                video_url: form.video_url || null,
                attachment_url: form.attachment_url || null,
                attachment_name: form.attachment_name || null,
                meta_title: form.meta_title,
                meta_description: form.meta_description,
                tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean),
                category_id: form.category_id || null,
                subcategory_id: form.subcategory_id || null,
                is_published: false,
                published_at: null,
                author_name: user?.name || 'Admin',
                read_time_minutes: estimateReadTime(contentHtml),
                created_at: new Date().toISOString(),
                updated_at: new Date().toISOString(),
              })}
              className="flex items-center gap-2 px-4 py-2.5 rounded-xl border
                border-gray-200 dark:border-white/[0.08]
                text-gray-600 dark:text-gray-400
                hover:bg-gray-50 dark:hover:bg-white/[0.05]
                transition text-sm font-medium"
            >
              <ExternalLink className="w-4 h-4" /> Preview
            </button>
            <button
              onClick={() => handleSave(false)}
              disabled={saving}
              className="flex items-center gap-2 px-4 py-2.5 rounded-xl border
                border-gray-200 dark:border-white/[0.08]
                text-gray-600 dark:text-gray-400
                hover:bg-gray-50 dark:hover:bg-white/[0.05]
                transition text-sm font-medium disabled:opacity-50"
            >
              <Save className="w-4 h-4" /> Save Draft
            </button>
            <button
              onClick={() => handleSave(true)}
              disabled={saving}
              className="flex items-center gap-2 px-4 py-2.5 rounded-xl
                bg-violet-600 hover:bg-violet-700 text-white
                transition text-sm font-medium shadow-lg shadow-violet-900/30 disabled:opacity-50"
            >
              <Globe className="w-4 h-4" />
              {saving ? 'Saving…' : 'Publish'}
            </button>
          </div>
        </div>

        <div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
          <div className="xl:col-span-2 space-y-4">
            <div>
              <label className={labelCls}>Article Title <span className="text-red-400">*</span></label>
              <input
                type="text" value={form.title}
                onChange={(e) => handleTitleChange(e.target.value)}
                className={inputCls + ' text-lg font-semibold'}
                placeholder="Write an engaging title…"
              />
            </div>

            <div>
              <label className={labelCls}>URL Slug <span className="text-red-400">*</span></label>
              <div className="flex items-center gap-2">
                <span className="text-xs text-gray-400 flex-shrink-0">/blog/</span>
                <input
                  type="text" value={form.slug}
                  onChange={(e) => { setForm((p) => ({ ...p, slug: e.target.value })); setSlugManual(true); }}
                  className={inputCls}
                  placeholder="my-article-title"
                />
              </div>
            </div>

            <div className="grid grid-cols-2 gap-3">
              <div>
                <label className={labelCls}>Category</label>
                <select
                  value={form.category_id}
                  onChange={(e) => setForm((p) => ({ ...p, category_id: e.target.value, subcategory_id: '' }))}
                  className={inputCls}
                >
                  <option value="">None</option>
                  {categoryTree.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
                </select>
              </div>
              <div>
                <label className={labelCls}>Subcategory</label>
                <select
                  value={form.subcategory_id}
                  onChange={(e) => setForm((p) => ({ ...p, subcategory_id: e.target.value }))}
                  disabled={!form.category_id}
                  className={inputCls + (!form.category_id ? ' opacity-50 cursor-not-allowed' : '')}
                >
                  <option value="">None</option>
                  {subsForParent(form.category_id).map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
                </select>
              </div>
            </div>

            <div>
              <label className={labelCls}>Excerpt <span className="text-gray-400 normal-case font-normal">(auto-generated if empty)</span></label>
              <textarea
                value={form.excerpt}
                onChange={(e) => setForm((p) => ({ ...p, excerpt: e.target.value }))}
                className={inputCls + ' resize-none'} rows={2}
                placeholder="Short description shown in listings…"
              />
            </div>

            <div>
              <label className={labelCls}>Content <span className="text-red-400">*</span></label>

              <div className="flex flex-wrap gap-1 p-2 rounded-t-xl border border-b-0
                border-gray-200 dark:border-white/[0.08]
                bg-gray-50 dark:bg-[#0a1020]">
                <ToolBtn icon={<Heading1 className="w-4 h-4" />} onClick={() => exec('formatBlock', 'H1')} title="Heading 1" />
                <ToolBtn icon={<Heading2 className="w-4 h-4" />} onClick={() => exec('formatBlock', 'H2')} title="Heading 2" />
                <ToolBtn icon={<Heading3 className="w-4 h-4" />} onClick={() => exec('formatBlock', 'H3')} title="Heading 3" />
                <div className="w-px bg-gray-200 dark:bg-white/[0.08] mx-1" />
                <ToolBtn icon={<Bold className="w-4 h-4" />} onClick={() => exec('bold')} title="Bold" />
                <ToolBtn icon={<Italic className="w-4 h-4" />} onClick={() => exec('italic')} title="Italic" />
                <ToolBtn icon={<Underline className="w-4 h-4" />} onClick={() => exec('underline')} title="Underline" />
                <ToolBtn icon={<Strikethrough className="w-4 h-4" />} onClick={() => exec('strikeThrough')} title="Strikethrough" />
                <div className="w-px bg-gray-200 dark:bg-white/[0.08] mx-1" />
                <ToolBtn icon={<List className="w-4 h-4" />} onClick={() => exec('insertUnorderedList')} title="Bullet list" />
                <ToolBtn icon={<ListOrdered className="w-4 h-4" />} onClick={() => exec('insertOrderedList')} title="Numbered list" />
                <ToolBtn icon={<Quote className="w-4 h-4" />} onClick={() => exec('formatBlock', 'BLOCKQUOTE')} title="Quote" />
                <ToolBtn icon={<Code className="w-4 h-4" />} onClick={() => exec('formatBlock', 'PRE')} title="Code block" />
                <div className="w-px bg-gray-200 dark:bg-white/[0.08] mx-1" />
                <ToolBtn icon={<Link2 className="w-4 h-4" />} onClick={insertLink} title="Insert link" />
                <ToolBtn
                  icon={uploadingInlineImage ? <Loader2 className="w-4 h-4 animate-spin" /> : <ImageIcon className="w-4 h-4" />}
                  onClick={() => inlineImageInputRef.current?.click()}
                  title="Insert image"
                />
                <input
                  ref={inlineImageInputRef} type="file" accept="image/*" className="hidden"
                  onChange={(e) => { const f = e.target.files?.[0]; if (f) handleInlineImageUpload(f); e.target.value = ''; }}
                />
              </div>

              <div
                ref={editorRef}
                contentEditable
                onInput={handleEditorInput}
                className="w-full min-h-[420px] px-4 py-4 rounded-b-xl border
                  border-gray-200 dark:border-white/[0.08]
                  bg-white dark:bg-[#111d35] text-gray-900 dark:text-white
                  focus:outline-none focus:ring-2 focus:ring-violet-500/60 transition
                  prose prose-sm dark:prose-invert max-w-none
                  [&_h1]:text-2xl [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2
                  [&_h2]:text-xl [&_h2]:font-bold [&_h2]:mt-4 [&_h2]:mb-2
                  [&_h3]:text-lg [&_h3]:font-bold [&_h3]:mt-3 [&_h3]:mb-2
                  [&_blockquote]:border-l-4 [&_blockquote]:border-violet-400 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-gray-500
                  [&_pre]:bg-gray-100 dark:[&_pre]:bg-black/30 [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:font-mono [&_pre]:text-sm
                  [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6
                  [&_a]:text-violet-500 [&_a]:underline
                  [&_img]:rounded-xl [&_img]:my-3 [&_img]:max-w-full"
                style={{ scrollbarWidth: 'thin', scrollbarColor: 'rgba(139,92,246,0.3) transparent' }}
                data-placeholder="Start writing your article…"
                suppressContentEditableWarning
              />
              <p className="text-[11px] text-gray-400 mt-1">
                Rich text editor — formatting is preserved exactly as written and shown identically to readers
              </p>
            </div>
          </div>

          <div className="space-y-4">
            <div className="bg-white dark:bg-[#0d1526] rounded-2xl border border-gray-100 dark:border-white/[0.06] p-4">
              <h3 className="text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide mb-3 flex items-center gap-2">
                <ImageIcon className="w-3.5 h-3.5" /> Cover Image
              </h3>
              <button
                onClick={() => coverInputRef.current?.click()}
                disabled={uploadingCover}
                className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl
                  border-2 border-dashed border-gray-200 dark:border-white/[0.1]
                  hover:border-violet-400 dark:hover:border-violet-500 transition text-sm
                  text-gray-500 dark:text-gray-400 disabled:opacity-50"
              >
                {uploadingCover
                  ? <><Loader2 className="w-4 h-4 animate-spin" /> Uploading…</>
                  : <><ImageIcon className="w-4 h-4" /> Upload Cover</>
                }
              </button>
              <input
                ref={coverInputRef} type="file" accept="image/*" className="hidden"
                onChange={(e) => { const f = e.target.files?.[0]; if (f) handleCoverUpload(f); e.target.value = ''; }}
              />
              {form.cover_image_url && (
                <div className="relative mt-3">
                  <img src={form.cover_image_url} alt="Cover" className="w-full h-32 object-cover rounded-xl" />
                  <button
                    onClick={() => setForm((p) => ({ ...p, cover_image_url: '' }))}
                    className="absolute top-1.5 right-1.5 p-1 rounded-lg bg-black/60 text-white hover:bg-black/80 transition"
                  >
                    <X className="w-3.5 h-3.5" />
                  </button>
                </div>
              )}
            </div>

            <div className="bg-white dark:bg-[#0d1526] rounded-2xl border border-gray-100 dark:border-white/[0.06] p-4">
              <h3 className="text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide mb-3 flex items-center gap-2">
                <Video className="w-3.5 h-3.5" /> Video (optional)
              </h3>
              <button
                onClick={() => videoInputRef.current?.click()}
                disabled={uploadingVideo}
                className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl
                  border-2 border-dashed border-gray-200 dark:border-white/[0.1]
                  hover:border-violet-400 dark:hover:border-violet-500 transition text-sm
                  text-gray-500 dark:text-gray-400 disabled:opacity-50"
              >
                {uploadingVideo
                  ? <><Loader2 className="w-4 h-4 animate-spin" /> Uploading…</>
                  : <><Video className="w-4 h-4" /> Upload Video</>
                }
              </button>
              <input
                ref={videoInputRef} type="file" accept="video/*" className="hidden"
                onChange={(e) => { const f = e.target.files?.[0]; if (f) handleVideoUpload(f); e.target.value = ''; }}
              />
              {form.video_url && (
                <div className="mt-3 flex items-center justify-between text-xs bg-gray-50 dark:bg-white/[0.03] rounded-lg px-3 py-2">
                  <span className="truncate text-gray-600 dark:text-gray-300">Video attached ✓</span>
                  <button onClick={() => setForm((p) => ({ ...p, video_url: '' }))} className="text-red-400 hover:text-red-500 flex-shrink-0 ml-2">
                    <X className="w-3.5 h-3.5" />
                  </button>
                </div>
              )}
            </div>

            <div className="bg-white dark:bg-[#0d1526] rounded-2xl border border-gray-100 dark:border-white/[0.06] p-4">
              <h3 className="text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide mb-3 flex items-center gap-2">
                <Paperclip className="w-3.5 h-3.5" /> Downloadable File (optional)
              </h3>
              <button
                onClick={() => attachmentInputRef.current?.click()}
                disabled={uploadingAttachment}
                className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl
                  border-2 border-dashed border-gray-200 dark:border-white/[0.1]
                  hover:border-violet-400 dark:hover:border-violet-500 transition text-sm
                  text-gray-500 dark:text-gray-400 disabled:opacity-50"
              >
                {uploadingAttachment
                  ? <><Loader2 className="w-4 h-4 animate-spin" /> Uploading…</>
                  : <><Paperclip className="w-4 h-4" /> Upload PDF / File</>
                }
              </button>
              <input
                ref={attachmentInputRef} type="file" className="hidden"
                onChange={(e) => { const f = e.target.files?.[0]; if (f) handleAttachmentUpload(f); e.target.value = ''; }}
              />
              {form.attachment_url && (
                <div className="mt-3 flex items-center justify-between text-xs bg-gray-50 dark:bg-white/[0.03] rounded-lg px-3 py-2">
                  <span className="truncate text-gray-600 dark:text-gray-300">{form.attachment_name}</span>
                  <button onClick={() => setForm((p) => ({ ...p, attachment_url: '', attachment_name: '' }))} className="text-red-400 hover:text-red-500 flex-shrink-0 ml-2">
                    <X className="w-3.5 h-3.5" />
                  </button>
                </div>
              )}
            </div>

            <div className="bg-white dark:bg-[#0d1526] rounded-2xl border border-gray-100 dark:border-white/[0.06] p-4">
              <h3 className="text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide mb-3 flex items-center gap-2">
                <Tag className="w-3.5 h-3.5" /> Tags
              </h3>
              <input
                type="text" value={form.tags}
                onChange={(e) => setForm((p) => ({ ...p, tags: e.target.value }))}
                className={inputCls}
                placeholder="math, tips, exam-prep"
              />
              {form.tags && (
                <div className="flex flex-wrap gap-1.5 mt-2">
                  {form.tags.split(',').map((t) => t.trim()).filter(Boolean).map((tag) => (
                    <span key={tag} className="text-[11px] px-2 py-0.5 rounded-full
                      bg-violet-100 dark:bg-violet-500/20 text-violet-700 dark:text-violet-400 font-medium">
                      {tag}
                    </span>
                  ))}
                </div>
              )}
            </div>

            <div className="bg-white dark:bg-[#0d1526] rounded-2xl border border-gray-100 dark:border-white/[0.06] p-4">
              <h3 className="text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide mb-3 flex items-center gap-2">
                <Globe className="w-3.5 h-3.5" /> SEO Settings
              </h3>
              <div className="space-y-3">
                <div>
                  <label className={labelCls}>Meta Title</label>
                  <input
                    type="text" value={form.meta_title}
                    onChange={(e) => setForm((p) => ({ ...p, meta_title: e.target.value }))}
                    className={inputCls} placeholder={form.title || 'SEO title'} maxLength={60}
                  />
                  <div className="flex justify-between mt-1">
                    <p className="text-[11px] text-gray-400">Ideal: 50–60 chars</p>
                    <span className={`text-[11px] font-medium ${form.meta_title.length > 60 ? 'text-red-400' : 'text-gray-400'}`}>
                      {form.meta_title.length}/60
                    </span>
                  </div>
                </div>
                <div>
                  <label className={labelCls}>Meta Description</label>
                  <textarea
                    value={form.meta_description}
                    onChange={(e) => setForm((p) => ({ ...p, meta_description: e.target.value }))}
                    className={inputCls + ' resize-none'} rows={3} maxLength={160}
                    placeholder="Compelling description for search engines…"
                  />
                  <div className="flex justify-between mt-1">
                    <p className="text-[11px] text-gray-400">Ideal: 120–160 chars</p>
                    <span className={`text-[11px] font-medium ${form.meta_description.length > 160 ? 'text-red-400' : 'text-gray-400'}`}>
                      {form.meta_description.length}/160
                    </span>
                  </div>
                </div>
              </div>

              <div className="mt-4 p-3 rounded-xl bg-gray-50 dark:bg-[#060b17] border border-gray-200 dark:border-white/[0.06]">
                <p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wide mb-2">Google Preview</p>
                <p className="text-blue-600 dark:text-blue-400 text-sm font-medium truncate">
                  {form.meta_title || form.title || 'Article Title'}
                </p>
                <p className="text-[11px] text-green-600 dark:text-green-400 mt-0.5">
                  {typeof window !== 'undefined' ? window.location.origin.replace(/^https?:\/\//, '') : 'yourdomain.com'}/blog/{form.slug || 'your-slug'}
                </p>
                <p className="text-[11px] text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2">
                  {form.meta_description || form.excerpt || 'Meta description appears here…'}
                </p>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
        <div>
          <h1 className="text-2xl font-bold text-gray-800 dark:text-white">Manage Blogs</h1>
          <p className="text-sm text-gray-400 mt-0.5">
            {total} article{total !== 1 ? 's' : ''} total
          </p>
        </div>
        <button
          onClick={openNew}
          className="flex items-center gap-2 px-4 py-2.5 rounded-xl
            bg-violet-600 hover:bg-violet-700 text-white
            transition text-sm font-medium shadow-lg shadow-violet-900/30"
        >
          <Plus className="w-4 h-4" /> New Article
        </button>
      </div>

      <div className="relative">
        <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
        <input
          type="text" value={search}
          onChange={(e) => setSearch(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
          placeholder="Search by title…"
          className={inputCls + ' pl-10'}
        />
      </div>

      {loading ? (
        <div className="flex justify-center py-16">
          <div className="w-10 h-10 border-[3px] border-violet-500/30 border-t-violet-500 rounded-full animate-spin" />
        </div>
      ) : blogs.length === 0 ? (
        <div className="text-center py-20">
          <FileText className="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" />
          <p className="text-gray-500 dark:text-gray-400 text-sm">
            {search ? 'No articles match your search.' : 'No articles yet. Create your first one!'}
          </p>
        </div>
      ) : (
        <>
          <div className="space-y-3">
            {blogs.map((blog) => (
              <div
                key={blog.id}
                className="bg-white dark:bg-[#0d1526] rounded-2xl border border-gray-100 dark:border-white/[0.06]
                  p-4 flex flex-col sm:flex-row gap-4 hover:shadow-md dark:hover:shadow-violet-900/10 transition"
              >
                {blog.cover_image_url && (
                  <img src={blog.cover_image_url} alt={blog.title}
                    className="w-full sm:w-28 h-20 object-cover rounded-xl flex-shrink-0" loading="lazy" />
                )}
                <div className="flex-1 min-w-0">
                  <div className="flex items-start justify-between gap-2 mb-1">
                    <h3 className="text-sm font-bold text-gray-800 dark:text-white line-clamp-1">{blog.title}</h3>
                    <span className={`text-[11px] px-2 py-0.5 rounded-full font-semibold flex-shrink-0 ${
                      blog.is_published
                        ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400'
                        : 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400'
                    }`}>
                      {blog.is_published ? 'Published' : 'Draft'}
                    </span>
                  </div>
                  <p className="text-xs text-gray-400 font-mono mb-1">/blog/{blog.slug}</p>
                  {blog.excerpt && <p className="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mb-2">{blog.excerpt}</p>}
                  <div className="flex items-center gap-3 flex-wrap">
                    {(blog.tags || []).slice(0, 3).map((tag) => (
                      <span key={tag} className="text-[11px] px-2 py-0.5 rounded-full
                        bg-violet-100 dark:bg-violet-500/20 text-violet-700 dark:text-violet-400 font-medium">
                        {tag}
                      </span>
                    ))}
                    <span className="text-[11px] text-gray-400 flex items-center gap-1 ml-auto">
                      <Calendar className="w-3 h-3" />
                      {new Date(blog.updated_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
                    </span>
                  </div>
                </div>
                <div className="flex sm:flex-col gap-1.5 flex-shrink-0 sm:w-9">
                  <button onClick={() => openPreview(blog.id)}
                    className="w-9 h-9 flex items-center justify-center rounded-lg text-violet-500 hover:bg-violet-50 dark:hover:bg-violet-500/10 transition" title="Preview">
                    <ExternalLink className="w-4 h-4" />
                  </button>
                  <button onClick={() => openEdit(blog.id)}
                    className="w-9 h-9 flex items-center justify-center rounded-lg text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition" title="Edit">
                    <Edit2 className="w-4 h-4" />
                  </button>
                  <button onClick={() => handleTogglePublish(blog)}
                    className="w-9 h-9 flex items-center justify-center rounded-lg text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 transition" title={blog.is_published ? 'Unpublish' : 'Publish'}>
                    {blog.is_published ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
                  </button>
                  <button onClick={() => setDeleteId(blog.id)}
                    className="w-9 h-9 flex items-center justify-center rounded-lg text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition" title="Delete">
                    <Trash2 className="w-4 h-4" />
                  </button>
                </div>
              </div>
            ))}
          </div>

          {totalPages > 1 && (
            <div className="flex items-center justify-between pt-2">
              <p className="text-xs text-gray-400">
                Page {page + 1} of {totalPages} · {total} total articles
              </p>
              <div className="flex gap-2">
                <button
                  onClick={() => setPage((p) => Math.max(0, p - 1))}
                  disabled={page === 0}
                  className="p-2 rounded-lg border border-gray-200 dark:border-white/[0.08]
                    text-gray-500 dark:text-gray-400 disabled:opacity-30 disabled:cursor-not-allowed
                    hover:bg-gray-50 dark:hover:bg-white/[0.05] transition"
                >
                  <ChevronLeft className="w-4 h-4" />
                </button>
                <button
                  onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
                  disabled={page >= totalPages - 1}
                  className="p-2 rounded-lg border border-gray-200 dark:border-white/[0.08]
                    text-gray-500 dark:text-gray-400 disabled:opacity-30 disabled:cursor-not-allowed
                    hover:bg-gray-50 dark:hover:bg-white/[0.05] transition"
                >
                  <ChevronRight className="w-4 h-4" />
                </button>
              </div>
            </div>
          )}
        </>
      )}

      <Modal isOpen={!!deleteId} onClose={() => setDeleteId(null)} title="Delete Article">
        <div className="text-center py-2">
          <div className="w-14 h-14 bg-red-100 dark:bg-red-500/15 rounded-full flex items-center justify-center mx-auto mb-4">
            <Trash2 className="w-6 h-6 text-red-500" />
          </div>
          <p className="text-sm text-gray-500 dark:text-gray-400 mb-6 max-w-xs mx-auto">
            This will permanently delete this article. This cannot be undone.
          </p>
          <div className="flex justify-center gap-3">
            <button onClick={() => setDeleteId(null)} className="px-5 py-2.5 rounded-xl border border-gray-200 dark:border-white/10 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/[0.05] transition text-sm">
              Cancel
            </button>
            <button onClick={handleDelete} className="px-5 py-2.5 rounded-xl bg-red-600 hover:bg-red-700 text-white text-sm font-medium transition">
              Delete
            </button>
          </div>
        </div>
      </Modal>

      {/* ── Full WordPress-style Preview Modal ── */}
      {previewBlog && (
        <div className="fixed inset-0 z-[100] flex items-start justify-center overflow-y-auto bg-black/70 backdrop-blur-sm p-4 sm:p-8">
          <div className="w-full max-w-3xl bg-white dark:bg-[#0d1526] rounded-2xl shadow-2xl my-4 overflow-hidden">
            {/* Preview toolbar */}
            <div className="sticky top-0 z-10 flex items-center justify-between px-5 py-3
              bg-gray-900 dark:bg-black text-white">
              <div className="flex items-center gap-2 text-xs">
                <ExternalLink className="w-3.5 h-3.5" />
                <span className="font-medium">Live Preview</span>
                <span className="text-gray-400">— exactly how readers will see this article</span>
              </div>
              <button
                onClick={() => setPreviewBlog(null)}
                className="p-1.5 rounded-lg hover:bg-white/10 transition"
              >
                <X className="w-4 h-4" />
              </button>
            </div>

            {/* Rendered article — same structure as public blog page will use */}
            <article className="px-6 sm:px-10 py-8">
              {previewBlog.cover_image_url && (
                <img
                  src={previewBlog.cover_image_url}
                  alt={previewBlog.title}
                  className="w-full h-56 sm:h-72 object-cover rounded-2xl mb-6"
                />
              )}

              {/* Category / tags */}
              <div className="flex items-center gap-2 flex-wrap mb-3">
                {(previewBlog.tags || []).map((tag) => (
                  <span key={tag} className="text-[11px] px-2.5 py-1 rounded-full
                    bg-violet-100 dark:bg-violet-500/20 text-violet-700 dark:text-violet-400 font-semibold">
                    {tag}
                  </span>
                ))}
              </div>

              {/* Title */}
              <h1 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-3 leading-tight">
                {previewBlog.title || 'Untitled Article'}
              </h1>

              {/* Meta row */}
              <div className="flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400 mb-8 pb-6
                border-b border-gray-100 dark:border-white/[0.08]">
                <span>{previewBlog.author_name || 'Admin'}</span>
                <span>·</span>
                <span>{new Date(previewBlog.updated_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</span>
                <span>·</span>
                <span>{previewBlog.read_time_minutes} min read</span>
              </div>

              {/* Video if present */}
              {previewBlog.video_url && (
                <video controls className="w-full rounded-2xl mb-6">
                  <source src={previewBlog.video_url} />
                </video>
              )}

              {/* Main content — rendered exactly as written */}
              <div
                className="prose prose-lg dark:prose-invert max-w-none
                  [&_h1]:text-2xl [&_h1]:font-bold [&_h1]:mt-6 [&_h1]:mb-3
                  [&_h2]:text-xl [&_h2]:font-bold [&_h2]:mt-6 [&_h2]:mb-3
                  [&_h3]:text-lg [&_h3]:font-bold [&_h3]:mt-4 [&_h3]:mb-2
                  [&_p]:mb-4 [&_p]:leading-relaxed
                  [&_blockquote]:border-l-4 [&_blockquote]:border-violet-400 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-gray-500 [&_blockquote]:my-4
                  [&_pre]:bg-gray-100 dark:[&_pre]:bg-black/30 [&_pre]:p-4 [&_pre]:rounded-xl [&_pre]:font-mono [&_pre]:text-sm [&_pre]:my-4 [&_pre]:overflow-x-auto
                  [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:mb-4 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:mb-4
                  [&_a]:text-violet-500 [&_a]:underline
                  [&_img]:rounded-xl [&_img]:my-4 [&_img]:max-w-full"
                dangerouslySetInnerHTML={{ __html: previewBlog.content || '<p class="text-gray-400 italic">No content yet…</p>' }}
              />

              {/* Downloadable attachment */}
              {previewBlog.attachment_url && (
                <a
                  href={previewBlog.attachment_url}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="flex items-center gap-3 mt-8 p-4 rounded-xl
                    bg-violet-50 dark:bg-violet-500/10
                    border border-violet-200 dark:border-violet-500/25
                    hover:bg-violet-100 dark:hover:bg-violet-500/15 transition"
                >
                  <Paperclip className="w-5 h-5 text-violet-500 flex-shrink-0" />
                  <div>
                    <p className="text-sm font-semibold text-violet-700 dark:text-violet-300">Download Attachment</p>
                    <p className="text-xs text-violet-500 dark:text-violet-400">{previewBlog.attachment_name}</p>
                  </div>
                </a>
              )}
            </article>
          </div>
        </div>
      )}
    </div>
  );
};

export default ManageBlogs;