javascript
//src/app/(admin)/admin/blog/components/Composer.tsx
"use client";
import { useState, useEffect } from "react";
import {
Save, Eye, Edit3, Layout,
Loader2, CheckCircle2, Globe, FileText,
ArrowLeft, AlertCircle
} from 'lucide-react';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';
import ComposerMetadata from './ComposerMetadata';
import { uploadToR2 } from '@/app/lib/actions/media';
import { savePost } from '@/app/lib/actions/posts';
export default function BlogComposer() {
// --- Core State ---
const [content, setContent] = useState("");
const [meta, setMeta] = useState({
id: "", // Needed for updates
title: "",
slug: "",
excerpt: "",
category: "Lab Report",
cover_image_url: "",
published: false
});
const [isUploading, setIsUploading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [view, setView] = useState<'split' | 'edit' | 'preview'>('split');
const [isMounted, setIsMounted] = useState(false);
useEffect(() => setIsMounted(true), []);
// --- Logic: Auto-generate slug from title ---
useEffect(() => {
if (meta.title) {
const generatedSlug = meta.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
setMeta(prev => ({ ...prev, slug: generatedSlug }));
}
}, [meta.title]);
// --- Logic: Cloudflare R2 Upload ---
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsUploading(true);
try {
const formData = new FormData();
formData.append("file", file);
// Call server action
const publicUrl = await uploadToR2(formData);
// CRITICAL FIX: Update state with the R2 URL
setMeta(prev => ({ ...prev, cover_image_url: publicUrl }));
} catch (err) {
console.error("Upload failed:", err);
alert("Cloudflare R2 Upload Failed. Check CORS settings.");
} finally {
setIsUploading(false);
}
};
// --- Logic: Database Commit ---
const handleSave = async () => {
if (!meta.title || !meta.slug) {
alert("Title and Slug are required.");
return;
}
setIsSaving(true);
try {
const result = await savePost({
...meta,
content
});
// If new post, save the returned ID into state
if (result?.id) setMeta(prev => ({ ...prev, id: result.id }));
alert("Post committed to ElLabs Archive successfully.");
} catch (err) {
console.error("Save error:", err);
alert("Failed to save to database.");
} finally {
setIsSaving(false);
}
};
if (!isMounted) return null;
return (
<>
<style dangerouslySetInnerHTML={{__html: `body { overflow: hidden; }` }} />
<div className="flex h-screen bg-white dark:bg-[#050505] pt-16 font-sans">
{/* --- LEFT: METADATA INSPECTOR --- */}
<ComposerMetadata
meta={meta}
setMeta={setMeta}
onImageUpload={handleImageUpload}
isUploading={isUploading}
/>
{/* --- RIGHT: MASTER WORKSPACE --- */}
<div className="flex-1 flex flex-col min-w-0">
{/* Studio Toolbar */}
<div className="h-16 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between px-8 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-xl z-20">
{/* View Toggles */}
<div className="flex items-center gap-1.5 p-1 bg-zinc-100 dark:bg-zinc-900 rounded-xl">
<button
onClick={() => setView('edit')}
className={`p-2 rounded-lg transition-all ${view === 'edit' ? 'bg-white dark:bg-zinc-800 text-blue-600 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'}`}
>
<Edit3 className="w-4 h-4" />
</button>
<button
onClick={() => setView('split')}
className={`p-2 rounded-lg transition-all ${view === 'split' ? 'bg-white dark:bg-zinc-800 text-blue-600 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'}`}
>
<Layout className="w-4 h-4 rotate-90" />
</button>
<button
onClick={() => setView('preview')}
className={`p-2 rounded-lg transition-all ${view === 'preview' ? 'bg-white dark:bg-zinc-800 text-blue-600 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'}`}
>
<Eye className="w-4 h-4" />
</button>
</div>
{/* Status & Actions */}
<div className="flex items-center gap-4">
{/* Status Toggle */}
<div className="flex items-center gap-3 mr-2 px-4 border-r border-zinc-200 dark:border-zinc-800">
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-400">Status</span>
<button
onClick={() => setMeta(prev => ({ ...prev, published: !prev.published }))}
className={`px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-[0.2em] transition-all border ${
meta.published
? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20 shadow-[0_0_12px_rgba(16,185,129,0.1)]'
: 'bg-zinc-100 dark:bg-zinc-800 text-zinc-400 border-zinc-200 dark:border-zinc-700'
}`}
>
{meta.published ? 'Live' : 'Draft'}
</button>
</div>
<button
onClick={handleSave}
disabled={isSaving}
className="flex items-center gap-2 px-6 py-2.5 bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 text-[10px] font-black uppercase tracking-[0.2em] rounded-xl hover:scale-105 active:scale-95 transition-all shadow-xl disabled:opacity-50"
>
{isSaving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
Commit Changes
</button>
</div>
</div>
{/* Editor/Preview Body */}
<div className="flex-1 flex overflow-hidden">
{/* RAW INPUT (Left) */}
{(view === 'edit' || view === 'split') && (
<div className="flex-1 overflow-hidden border-r border-zinc-100 dark:border-zinc-900 bg-white dark:bg-zinc-950">
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Write your Lab Report in Markdown..."
className="w-full h-full p-12 bg-transparent outline-none resize-none font-mono text-[13px] leading-relaxed text-zinc-700 dark:text-zinc-300 custom-scrollbar"
/>
</div>
)}
{/* HIGH-FIDELITY PREVIEW (Right) */}
{(view === 'preview' || view === 'split') && (
<div className="flex-1 overflow-y-auto p-12 lg:p-20 bg-zinc-50 dark:bg-[#080808] custom-scrollbar">
<article className="max-w-2xl mx-auto prose prose-zinc dark:prose-invert
prose-headings:tracking-tighter prose-headings:font-extrabold
prose-p:text-zinc-600 dark:prose-p:text-zinc-400 prose-p:leading-relaxed
prose-pre:bg-zinc-950 prose-pre:border prose-pre:border-zinc-800 prose-pre:rounded-2xl
">
<header className="mb-12 border-b border-zinc-200 dark:border-zinc-800 pb-12">
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-500 mb-4 block">
{meta.category}
</span>
<h1 className="text-4xl lg:text-5xl font-black mb-6 !leading-[1.1]">
{meta.title || "Untitled Report"}
</h1>
<p className="text-xl text-zinc-400 italic font-medium">
{meta.excerpt ? `"${meta.excerpt}"` : "Enter an excerpt in the sidebar..."}
</p>
</header>
{meta.cover_image_url && (
<div className="relative aspect-video rounded-[2.5rem] overflow-hidden mb-12 shadow-2xl border border-zinc-200 dark:border-zinc-800">
<img src={meta.cover_image_url} alt="Cover Preview" className="object-cover w-full h-full" />
</div>
)}
<ReactMarkdown>{content}</ReactMarkdown>
</article>
</div>
)}
</div>
{/* Bottom HUD Bar */}
<div className="h-10 border-t border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 flex items-center justify-between px-8 text-[9px] font-black uppercase tracking-[0.2em] text-zinc-400">
<div className="flex gap-6">
<span>Characters: {content.length}</span>
<span>Words: {content.trim() ? content.trim().split(/\s+/).length : 0}</span>
</div>
<div className="flex items-center gap-2">
<Globe className="w-3 h-3" />
<span>Cloudflare R2 Synced</span>
</div>
</div>
</div>
</div>
</>
);
}
