| // 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.