tahamajs/IE / Projects /front /src /components /article /CreateArticleForm.tsx
tahamajs's picture
download
raw
28.9 kB
// src/components/article/CreateArticleForm.tsx
import { useCallback, useRef, useState, Suspense, lazy } from "react";
import { useCreateArticle } from "@/hooks/useCreateArticle";
import { ArticleSelector } from "./ArticleSelector";
import { AcademicPreview } from "./AcademicPreview";
import { importDocx } from "@/lib/import-utils";
import {
exportToDocx, exportToPdf, exportToMarkdown, exportToLatex, exportToHtml,
} from "@/lib/export-utils";
import { toast } from "react-toastify";
import {
Tag, X, BookOpen, Globe, FileText, Eye, Loader2,
Download, Upload, ChevronDown, Lightbulb, Keyboard, PanelRightClose, PanelRightOpen,
Sparkles, CheckCircle2
} from "lucide-react";
import { useAuth } from "@/context/AuthContext";
import { motion, AnimatePresence } from "framer-motion";
// Lazy‑load ReactQuill
const ReactQuill = lazy(() => import("react-quill-new"));
// Quill modules & custom image handler
const quillModules = {
toolbar: {
container: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
[{ list: "ordered" }, { list: "bullet" }, { indent: "-1" }, { indent: "+1" }],
["link", "code-block", "image"],
[{ align: [] }],
["clean"],
],
handlers: {
image: function () {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
try {
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const res = await (await import("@/lib/api")).default.post("/media/upload", {
file: base64, fileName: file.name,
});
const url = res.data.url || res.data.result?.url;
const quill = (this as any).quill;
const range = quill.getSelection(true);
quill.insertEmbed(range.index, "image", url);
} catch (err) {
toast.error("Image upload failed. Try a smaller image.");
}
};
input.click();
},
},
},
};
interface CreateArticleFormProps {
showPreview?: boolean;
onSaveComplete?: () => void;
}
export function CreateArticleForm({ showPreview: initialShowPreview = false, onSaveComplete }: CreateArticleFormProps) {
const { user } = useAuth();
const {
title, setTitle,
abstract, setAbstract,
body, setBody,
tags, tagInput, setTagInput,
status, setStatus,
relatedIds, setRelatedIds,
existingTags,
error, fieldErrors,
submitting,
addTag, removeTag, submit, saveDraft,
charCounts,
} = useCreateArticle();
const quillRef = useRef<any>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [importing, setImporting] = useState(false);
const [exportOpen, setExportOpen] = useState(false);
const [showTips, setShowTips] = useState(true);
const [showPreview, setShowPreview] = useState(initialShowPreview);
const [focusedField, setFocusedField] = useState<string | null>(null);
const insertLatex = useCallback((displayMode: boolean) => {
const quill = quillRef.current?.getEditor();
if (!quill) return;
const range = quill.getSelection(true);
const prefix = displayMode ? "$$" : "$";
const text = quill.getText(range.index, range.length).trim();
if (text) {
quill.deleteText(range.index, range.length);
quill.insertText(range.index, `${prefix}${text}${prefix}`);
quill.setSelection(range.index + prefix.length + text.length + prefix.length, 0);
} else {
quill.insertText(range.index, `${prefix}${prefix}`);
quill.setSelection(range.index + prefix.length, 0);
}
}, []);
const handleImportDocx = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setImporting(true);
try {
const html = await importDocx(file);
const quill = quillRef.current?.getEditor();
if (quill) {
quill.clipboard.dangerouslyPasteHTML(html);
} else {
setBody((prev) => prev + html);
}
toast.success("Document imported successfully! 🎉");
} catch (err) {
toast.error("Failed to import DOCX. The file may be corrupted.");
} finally {
setImporting(false);
if (fileInputRef.current) fileInputRef.current.value = "";
}
};
const handleExport = (format: string) => {
setExportOpen(false);
if (!body.trim()) {
toast.warning("Nothing to export. Write something first.");
return;
}
try {
switch (format) {
case "docx": exportToDocx(title, body); break;
case "pdf": exportToPdf(body, title); break;
case "md": exportToMarkdown(title, body); break;
case "tex": exportToLatex(title, body); break;
case "html": exportToHtml(title, body); break;
}
toast.success(`Exported beautifully as ${format.toUpperCase()} 🚀`);
} catch (err) {
toast.error("Export failed");
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
saveDraft();
toast.success("Draft securely saved! 💾", { position: "bottom-right", autoClose: 2000 });
}
};
return (
<div className="w-full h-full bg-slate-50/50 dark:bg-slate-900/50 backdrop-blur-3xl rounded-[2rem] overflow-hidden flex flex-col" onKeyDown={handleKeyDown}>
{/* Sticky top bar - Premium Glass */}
<div className="sticky top-0 z-30 bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border-b border-white/40 dark:border-white/10 shadow-sm">
<div className="max-w-[1800px] mx-auto px-4 sm:px-6 py-4 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
<Sparkles className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-xl font-black bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300 bg-clip-text text-transparent hidden sm:block">
Authoring Studio
</h1>
<div className="hidden sm:flex items-center gap-1.5 mt-0.5">
<Keyboard className="w-3.5 h-3.5 text-slate-400" />
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">
Ctrl+S to save draft
</span>
</div>
</div>
</div>
<div className="flex items-center gap-3 bg-white/50 dark:bg-slate-800/50 p-1.5 rounded-2xl border border-white/50 dark:border-white/10 backdrop-blur-md">
<button
onClick={() => setShowPreview(!showPreview)}
className={`px-4 py-2 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${
showPreview
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300 shadow-inner'
: 'hover:bg-white dark:hover:bg-slate-700 text-slate-600 dark:text-slate-300'
}`}
title={showPreview ? "Hide preview" : "Show preview"}
>
<Eye className="w-4 h-4" />
<span className="hidden sm:inline">{showPreview ? "Editing" : "Preview"}</span>
</button>
<div className="w-px h-6 bg-slate-200 dark:bg-slate-700 hidden sm:block" />
<input ref={fileInputRef} type="file" accept=".docx" className="hidden" onChange={handleImportDocx} />
<button
onClick={() => fileInputRef.current?.click()}
disabled={importing}
className="px-4 py-2 rounded-xl text-sm font-bold text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 transition-all flex items-center gap-2"
title="Import Word document"
>
{importing ? <Loader2 className="animate-spin w-4 h-4" /> : <Upload className="w-4 h-4" />}
<span className="hidden sm:inline">Import</span>
</button>
<div className="relative">
<button
onClick={() => setExportOpen(!exportOpen)}
className="px-4 py-2 rounded-xl text-sm font-bold text-slate-600 dark:text-slate-300 hover:bg-white dark:hover:bg-slate-700 transition-all flex items-center gap-2"
title="Export article"
>
<Download className="w-4 h-4" />
<span className="hidden sm:inline">Export</span>
<ChevronDown className={`w-3 h-3 transition-transform ${exportOpen ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence>
{exportOpen && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 mt-3 w-56 premium-glass bg-white/90 dark:bg-slate-900/90 rounded-2xl shadow-2xl border border-white/50 dark:border-white/10 z-50 py-2 overflow-hidden backdrop-blur-xl"
>
{[
{ fmt: "docx", label: "📄 Word Document (.docx)" },
{ fmt: "pdf", label: "📕 PDF Document" },
{ fmt: "md", label: "📝 Markdown (.md)" },
{ fmt: "tex", label: "📐 LaTeX Source (.tex)" },
{ fmt: "html", label: "🌐 HTML File (.html)" },
].map((opt) => (
<button
key={opt.fmt}
onClick={() => handleExport(opt.fmt)}
className="w-full text-left px-5 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
{opt.label}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={saveDraft}
disabled={submitting}
className="px-5 py-2.5 rounded-2xl text-sm font-bold bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all flex items-center gap-2"
>
<FileText className="w-4 h-4" />
<span className="hidden sm:inline">Save Draft</span>
</button>
<button
type="submit"
disabled={submitting}
onClick={() => submit()}
className="px-6 py-2.5 rounded-2xl text-sm font-bold bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg shadow-indigo-500/30 hover:shadow-indigo-500/50 hover:scale-105 transition-all flex items-center gap-2 disabled:opacity-70 disabled:hover:scale-100"
>
{submitting ? <Loader2 className="animate-spin w-4 h-4" /> : <Globe className="w-4 h-4" />}
Publish Now
</button>
</div>
</div>
</div>
{/* Main editor grid */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 sm:p-6 lg:p-8">
<div className={`max-w-[1800px] mx-auto grid ${showPreview ? "lg:grid-cols-2 xl:grid-cols-3" : "lg:grid-cols-1 max-w-5xl"} gap-8 transition-all duration-500`}>
{/* Left column: editor */}
<div className={`${showPreview ? "xl:col-span-2" : ""} flex flex-col gap-6`}>
{/* Title Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`premium-glass rounded-[2rem] p-6 sm:p-8 transition-all duration-300 ${focusedField === 'title' ? 'ring-2 ring-indigo-500/50 shadow-xl shadow-indigo-500/10' : 'hover:shadow-lg'}`}
>
<input
type="text"
className="w-full bg-transparent text-3xl sm:text-4xl font-black text-slate-900 dark:text-white placeholder:text-slate-300 dark:placeholder:text-slate-700 border-none outline-none resize-none p-0 focus:ring-0"
value={title}
onChange={(e) => setTitle(e.target.value)}
onFocus={() => setFocusedField('title')}
onBlur={() => setFocusedField(null)}
placeholder="Give your masterpiece a title..."
maxLength={charCounts.maxTitle}
/>
<div className="flex justify-between items-center mt-4">
<div className="flex-1">
{fieldErrors.title && <p className="text-sm font-semibold text-red-500 flex items-center gap-1"><X className="w-4 h-4"/> {fieldErrors.title}</p>}
</div>
<div className={`text-xs font-bold px-3 py-1 rounded-full ${title.length > charCounts.maxTitle * 0.9 ? 'bg-orange-100 text-orange-600' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'}`}>
{charCounts.title} / {charCounts.maxTitle}
</div>
</div>
</motion.div>
{/* Abstract Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className={`premium-glass rounded-[2rem] p-6 sm:p-8 transition-all duration-300 ${focusedField === 'abstract' ? 'ring-2 ring-indigo-500/50 shadow-xl shadow-indigo-500/10' : 'hover:shadow-lg'}`}
>
<div className="flex items-center justify-between mb-4">
<label className="text-sm font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">Abstract Summary</label>
<div className={`text-xs font-bold px-3 py-1 rounded-full ${abstract.length > charCounts.maxAbstract * 0.9 ? 'bg-orange-100 text-orange-600' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'}`}>
{charCounts.abstract} / {charCounts.maxAbstract}
</div>
</div>
<textarea
className="w-full bg-transparent text-lg font-medium text-slate-700 dark:text-slate-300 placeholder:text-slate-300 dark:placeholder:text-slate-700 border-none outline-none resize-none p-0 focus:ring-0 min-h-[120px] custom-scrollbar"
value={abstract}
onChange={(e) => setAbstract(e.target.value)}
onFocus={() => setFocusedField('abstract')}
onBlur={() => setFocusedField(null)}
placeholder="Briefly summarize your brilliant research here in a few impactful sentences..."
maxLength={charCounts.maxAbstract}
/>
{fieldErrors.abstract && <p className="text-sm font-semibold text-red-500 flex items-center gap-1 mt-2"><X className="w-4 h-4"/> {fieldErrors.abstract}</p>}
</motion.div>
{/* Body Editor Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className={`premium-glass rounded-[2rem] p-2 sm:p-4 transition-all duration-300 flex flex-col min-h-[500px] ${focusedField === 'body' ? 'ring-2 ring-indigo-500/50 shadow-xl shadow-indigo-500/10' : 'hover:shadow-lg'}`}
onFocus={() => setFocusedField('body')}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setFocusedField(null);
}
}}
>
<div className="flex items-center justify-between p-4 border-b border-slate-200/50 dark:border-slate-700/50 mb-4 bg-white/40 dark:bg-slate-900/40 rounded-2xl">
<label className="text-sm font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">Main Content</label>
<div className="flex items-center gap-2">
<button type="button" onClick={() => insertLatex(true)}
className="text-xs font-bold px-3 py-1.5 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 shadow-sm border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
$$ Block Math
</button>
<button type="button" onClick={() => insertLatex(false)}
className="text-xs font-bold px-3 py-1.5 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 shadow-sm border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
$ Inline Math
</button>
</div>
</div>
<div className="flex-1 rounded-2xl overflow-hidden bg-white/60 dark:bg-slate-950/60 Editor-Container">
<Suspense fallback={<div className="h-full flex flex-col items-center justify-center text-slate-400 space-y-4 py-20"><Loader2 className="w-8 h-8 animate-spin text-indigo-500" /><p className="font-bold">Initializing Editor...</p></div>}>
<ReactQuill
ref={quillRef}
theme="snow"
value={body}
onChange={setBody}
className="h-full border-none"
modules={quillModules}
placeholder="Start composing your research article..."
/>
</Suspense>
</div>
{fieldErrors.body && <p className="text-sm font-semibold text-red-500 flex items-center gap-1 mt-4 px-4"><X className="w-4 h-4"/> {fieldErrors.body}</p>}
</motion.div>
{/* Metadata Footer (Tags, Citations, Status) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="premium-glass rounded-[2rem] p-6 sm:p-8 grid grid-cols-1 md:grid-cols-2 gap-8"
>
{/* Tags */}
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">
<Tag className="w-4 h-4" /> Discovery Tags
</label>
<div className="flex gap-2 bg-white/50 dark:bg-slate-900/50 p-1 rounded-2xl border border-slate-200/50 dark:border-slate-700/50">
<input
type="text"
className="flex-1 bg-transparent border-none outline-none px-4 py-2 text-sm font-medium focus:ring-0 placeholder:text-slate-400"
placeholder="e.g. quantum physics..."
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addTag(tagInput))}
list="existingTagsList"
/>
<button type="button" onClick={() => addTag(tagInput)} className="bg-indigo-600 text-white px-4 py-2 rounded-xl text-sm font-bold shadow-md hover:bg-indigo-700 transition-colors">
Add
</button>
</div>
<datalist id="existingTagsList">
{existingTags.map((t) => <option key={t} value={t} />)}
</datalist>
<AnimatePresence>
{tags.length > 0 && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex flex-wrap gap-2 pt-2">
{tags.map((t) => (
<span key={t} className="inline-flex items-center gap-1.5 bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/50 px-3 py-1.5 rounded-xl text-sm font-bold shadow-sm">
{t}
<button type="button" onClick={() => removeTag(t)} className="hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/50 rounded-full p-0.5 transition-colors"><X className="w-3 h-3" /></button>
</span>
))}
</motion.div>
)}
</AnimatePresence>
</div>
<div className="space-y-6">
{/* Citations */}
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">
<BookOpen className="w-4 h-4" /> Related Research
</label>
<div className="bg-white/50 dark:bg-slate-900/50 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 p-2">
<ArticleSelector selectedIds={relatedIds} onChange={setRelatedIds} placeholder="Search publications to cite..." />
</div>
</div>
{/* Status Toggle */}
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">
Visibility
</label>
<div className="flex p-1 bg-white/50 dark:bg-slate-900/50 rounded-2xl border border-slate-200/50 dark:border-slate-700/50">
<label className={`flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl cursor-pointer text-sm font-bold transition-all ${status === 'published' ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600 dark:text-indigo-400' : 'text-slate-500 hover:text-slate-700'}`}>
<input type="radio" checked={status === "published"} onChange={() => setStatus("published")} className="hidden" />
<Globe className="w-4 h-4" /> Published
</label>
<label className={`flex-1 flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl cursor-pointer text-sm font-bold transition-all ${status === 'draft' ? 'bg-white dark:bg-slate-800 shadow-sm text-slate-900 dark:text-white' : 'text-slate-500 hover:text-slate-700'}`}>
<input type="radio" checked={status === "draft"} onChange={() => setStatus("draft")} className="hidden" />
<FileText className="w-4 h-4" /> Draft
</label>
</div>
</div>
</div>
{error && (
<div className="md:col-span-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50 text-red-600 dark:text-red-400 p-4 rounded-2xl text-sm font-bold flex items-center gap-2 shadow-inner">
<X className="w-5 h-5 flex-shrink-0" />
{error}
</div>
)}
</motion.div>
</div>
{/* Right column: preview / tips */}
<div className="space-y-6 flex flex-col">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="premium-glass rounded-[2rem] overflow-hidden"
>
<button
onClick={() => setShowTips(!showTips)}
className="w-full flex items-center justify-between p-6 bg-gradient-to-r from-indigo-50/50 to-purple-50/50 dark:from-indigo-900/10 dark:to-purple-900/10 hover:bg-white/40 transition-colors"
>
<span className="flex items-center gap-3 text-base font-black text-slate-800 dark:text-slate-200">
<div className="p-2 bg-yellow-100 dark:bg-yellow-900/30 rounded-xl">
<Lightbulb className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
</div>
Writing Tips
</span>
<div className="p-2 bg-white/50 dark:bg-slate-800/50 rounded-full shadow-sm">
{showTips ? <PanelRightClose className="w-4 h-4 text-slate-500" /> : <PanelRightOpen className="w-4 h-4 text-slate-500" />}
</div>
</button>
<AnimatePresence>
{showTips && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="p-6 pt-2 space-y-4 text-sm font-medium text-slate-600 dark:text-slate-400 bg-white/20 dark:bg-slate-950/20">
<div className="flex items-start gap-3 bg-white/40 dark:bg-slate-900/40 p-3 rounded-2xl border border-white/50 dark:border-white/5">
<CheckCircle2 className="w-5 h-5 text-emerald-500 shrink-0 mt-0.5" />
<p><strong className="text-slate-800 dark:text-slate-200">Engaging Title:</strong> Keep it concise but descriptive enough to attract readers.</p>
</div>
<div className="flex items-start gap-3 bg-white/40 dark:bg-slate-900/40 p-3 rounded-2xl border border-white/50 dark:border-white/5">
<CheckCircle2 className="w-5 h-5 text-emerald-500 shrink-0 mt-0.5" />
<p><strong className="text-slate-800 dark:text-slate-200">Strong Abstract:</strong> Summarise the core problem, methodology, and key findings clearly.</p>
</div>
<div className="flex items-start gap-3 bg-white/40 dark:bg-slate-900/40 p-3 rounded-2xl border border-white/50 dark:border-white/5">
<CheckCircle2 className="w-5 h-5 text-emerald-500 shrink-0 mt-0.5" />
<p><strong className="text-slate-800 dark:text-slate-200">LaTeX Math:</strong> Use <code className="bg-slate-200 dark:bg-slate-800 text-indigo-600 dark:text-indigo-400 px-1.5 py-0.5 rounded-md text-xs font-mono font-bold mx-1">$$...$$</code> for display blocks and <code className="bg-slate-200 dark:bg-slate-800 text-indigo-600 dark:text-indigo-400 px-1.5 py-0.5 rounded-md text-xs font-mono font-bold mx-1">$...$</code> for inline equations.</p>
</div>
<div className="flex items-start gap-3 bg-white/40 dark:bg-slate-900/40 p-3 rounded-2xl border border-white/50 dark:border-white/5">
<CheckCircle2 className="w-5 h-5 text-emerald-500 shrink-0 mt-0.5" />
<p><strong className="text-slate-800 dark:text-slate-200">Citations:</strong> Search and link related platform research using the citation tool below.</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
<AnimatePresence>
{showPreview && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="flex-1 min-h-[600px] sticky top-28"
>
<AcademicPreview
title={title || "Your Masterpiece Title"}
abstract={abstract || "Your brilliantly composed abstract will appear right here..."}
body={body || "<p class='text-slate-400 italic'>Begin writing your article content to see the live preview...</p>"}
tags={tags}
authorName={user?.username || "Renowned Author"}
show={true}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
}

Xet Storage Details

Size:
28.9 kB
·
Xet hash:
88080c1865ece7dc9c2083cc92f7b3a91024e036ddb498dcacc7387488c56a91

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.