import React, { useState, useRef } from "react";
// PaperSubmissionApp.jsx
// Single-file React component (default export) using Tailwind CSS for styling.
// - Registration & Login (local state mock)
// - Abstract & Cover Letter inputs
// - Manuscript file upload (single file)
// - Figures upload (multiple files) with image previews
// - Basic client-side validation and friendly UI
// Notes:
// - This component is front-end only. Replace the `handleSubmit*` functions
// with real API calls (e.g. fetch/axios) to integrate with your backend.
// - Tailwind should be available in the host project.
// - shadcn/ui components can be swapped in for improved visuals.
export default function PaperSubmissionApp() {
const [view, setView] = useState("home"); // home | register | login | submit
// --- Auth state (mock) ---
const [user, setUser] = useState(null);
const [registerData, setRegisterData] = useState({ name: "", email: "", password: "", confirm: "" });
const [loginData, setLoginData] = useState({ email: "", password: "" });
const [authErrors, setAuthErrors] = useState("");
// --- Submission state ---
const [title, setTitle] = useState("");
const [abstract, setAbstract] = useState("");
const [coverLetter, setCoverLetter] = useState("");
const [manuscriptFile, setManuscriptFile] = useState(null);
const [figureFiles, setFigureFiles] = useState([]);
const [figPreviews, setFigPreviews] = useState([]);
const [submitMessage, setSubmitMessage] = useState("");
const fileInputRef = useRef(null);
const figInputRef = useRef(null);
// --- Helpers ---
function validateRegister() {
const { name, email, password, confirm } = registerData;
if (!name || !email || !password) return "All fields are required.";
if (password.length < 6) return "Password must be at least 6 characters.";
if (password !== confirm) return "Passwords do not match.";
return "";
}
function handleRegister(e) {
e.preventDefault();
const err = validateRegister();
if (err) return setAuthErrors(err);
// Mock register -> set user locally
setUser({ name: registerData.name, email: registerData.email });
setAuthErrors("");
setView("submit");
}
function handleLogin(e) {
e.preventDefault();
// Mock login: accept any non-empty email/password
if (!loginData.email || !loginData.password) return setAuthErrors("Provide email and password.");
setUser({ name: loginData.email.split("@")[0], email: loginData.email });
setAuthErrors("");
setView("submit");
}
function handleLogout() {
setUser(null);
setView("home");
}
// Manuscript upload (single file)
function handleManuscriptChange(e) {
const f = e.target.files && e.target.files[0];
setManuscriptFile(f || null);
}
// Figures: multiple + previews
function handleFiguresChange(e) {
const files = Array.from(e.target.files || []);
setFigureFiles(files);
// build previews for images (non-images will be shown with generic icon)
const previews = files.map((f) => {
const isImg = f.type.startsWith("image/");
if (!isImg) return { name: f.name, url: null, size: f.size };
return { name: f.name, url: URL.createObjectURL(f), size: f.size };
});
// revoke previous previews to avoid memory leak
figPreviews.forEach((p) => p.url && URL.revokeObjectURL(p.url));
setFigPreviews(previews);
}
function removeFigure(index) {
const files = [...figureFiles];
files.splice(index, 1);
setFigureFiles(files);
const previews = [...figPreviews];
const removed = previews.splice(index, 1)[0];
if (removed && removed.url) URL.revokeObjectURL(removed.url);
setFigPreviews(previews);
// also reset the file input so user can re-add
if (figInputRef.current) figInputRef.current.value = "";
}
// Basic client-side submission (pack into FormData and print to console)
async function handleSubmitManuscript(e) {
e.preventDefault();
if (!user) return setSubmitMessage("You must be logged in to submit.");
if (!title.trim()) return setSubmitMessage("Please provide a manuscript title.");
if (!abstract.trim()) return setSubmitMessage("Please provide an abstract.");
if (!manuscriptFile) return setSubmitMessage("Please upload the manuscript file (PDF, DOCX, etc.).");
setSubmitMessage("Preparing submission...");
// Build FormData -- replace with API endpoint
const fd = new FormData();
fd.append("title", title);
fd.append("abstract", abstract);
fd.append("coverLetter", coverLetter);
fd.append("manuscript", manuscriptFile);
figureFiles.forEach((f, i) => fd.append(`figure_${i + 1}`, f));
fd.append("submittedBy", user.email);
// Demo: create a JSON-friendly summary and log it
const summary = {
title,
abstract: abstract.slice(0, 300) + (abstract.length > 300 ? "..." : ""),
coverLetter: coverLetter.slice(0, 300) + (coverLetter.length > 300 ? "..." : ""),
manuscriptFile: manuscriptFile ? { name: manuscriptFile.name, size: manuscriptFile.size } : null,
figures: figureFiles.map((f) => ({ name: f.name, size: f.size })),
submittedBy: user.email,
submittedAt: new Date().toISOString(),
};
// Simulate upload delay
await new Promise((res) => setTimeout(res, 800));
console.log("FormData (simulated):", summary);
setSubmitMessage("Submission prepared (see console). Replace handleSubmitManuscript with a real API call to POST FormData.");
}
// --- UI pieces ---
const Nav = () => (
<header className="bg-white shadow-sm border-b">
<div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="text-2xl font-extrabold">PaperPortal</div>
<div className="text-sm text-slate-500">Submit and manage your manuscript</div>
</div>
<nav className="flex items-center gap-3">
{!user ? (
<>
<button onClick={() => setView("register")} className="btn">Register</button>
<button onClick={() => setView("login")} className="btn-outline">Login</button>
</>
) : (
<>
<div className="text-sm">Signed in as <strong>{user.name}</strong></div>
<button onClick={() => setView("submit")} className="btn">Submit</button>
<button onClick={handleLogout} className="btn-ghost">Logout</button>
</>
)}
</nav>
</div>
</header>
);
function Home() {
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-2">Welcome to PaperPortal</h1>
<p className="text-slate-600 mb-6">A minimal submission front-end: register/login, write an abstract & cover letter, upload manuscript and figures.</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card title="1. Register" body="Create an account so you can submit and track your manuscript." action={() => setView("register")} />
<Card title="2. Login" body="Sign in to continue to submission." action={() => setView("login")} />
<Card title="3. Submit" body="Start a new submission (requires login)." action={() => setView(user ? "submit" : "login")} />
</div>
</div>
);
}
function Card({ title, body, action }) {
return (
<div className="border rounded-2xl p-4 shadow-sm">
<h3 className="font-semibold mb-2">{title}</h3>
<p className="text-sm text-slate-600 mb-4">{body}</p>
<button onClick={action} className="px-3 py-2 rounded-md bg-slate-800 text-white text-sm">Open</button>
</div>
);
}
function RegisterView() {
return (
<div className="max-w-2xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Register</h2>
{authErrors && <div className="mb-3 text-red-600">{authErrors}</div>}
<form onSubmit={handleRegister} className="space-y-3">
<div>
<label className="block text-sm mb-1">Full name</label>
<input value={registerData.name} onChange={(e) => setRegisterData({ ...registerData, name: e.target.value })} className="input" />
</div>
<div>
<label className="block text-sm mb-1">Email</label>
<input value={registerData.email} onChange={(e) => setRegisterData({ ...registerData, email: e.target.value })} className="input" type="email" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm mb-1">Password</label>
<input value={registerData.password} onChange={(e) => setRegisterData({ ...registerData, password: e.target.value })} className="input" type="password" />
</div>
<div>
<label className="block text-sm mb-1">Confirm</label>
<input value={registerData.confirm} onChange={(e) => setRegisterData({ ...registerData, confirm: e.target.value })} className="input" type="password" />
</div>
</div>
<div className="flex gap-3">
<button type="submit" className="btn">Create account</button>
<button type="button" className="btn-ghost" onClick={() => setView("home")}>Back</button>
</div>
</form>
</div>
);
}
function LoginView() {
return (
<div className="max-w-2xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Login</h2>
{authErrors && <div className="mb-3 text-red-600">{authErrors}</div>}
<form onSubmit={handleLogin} className="space-y-3">
<div>
<label className="block text-sm mb-1">Email</label>
<input value={loginData.email} onChange={(e) => setLoginData({ ...loginData, email: e.target.value })} className="input" type="email" />
</div>
<div>
<label className="block text-sm mb-1">Password</label>
<input value={loginData.password} onChange={(e) => setLoginData({ ...loginData, password: e.target.value })} className="input" type="password" />
</div>
<div className="flex gap-3">
<button type="submit" className="btn">Sign in</button>
<button type="button" className="btn-ghost" onClick={() => setView("home")}>Back</button>
</div>
</form>
</div>
);
}
function SubmitView() {
return (
<div className="max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">New Submission</h2>
<form onSubmit={handleSubmitManuscript} className="space-y-4">
<div>
<label className="block text-sm mb-1">Manuscript Title</label>
<input value={title} onChange={(e) => setTitle(e.target.value)} className="input" placeholder="Short, descriptive title" />
</div>
<div>
<label className="block text-sm mb-1">Abstract</label>
<textarea value={abstract} onChange={(e) => setAbstract(e.target.value)} className="textarea h-32" placeholder="Paste the paper abstract here." />
<div className="text-xs text-slate-500 mt-1">{abstract.length} characters</div>
</div>
<div>
<label className="block text-sm mb-1">Cover letter (optional)</label>
<textarea value={coverLetter} onChange={(e) => setCoverLetter(e.target.value)} className="textarea h-28" placeholder="Optional: a short cover letter to the editor" />
</div>
<div>
<label className="block text-sm mb-1">Manuscript file</label>
<input ref={fileInputRef} type="file" accept=".pdf,.doc,.docx,.tex" onChange={handleManuscriptChange} />
{manuscriptFile && <div className="mt-2 text-sm">Selected: <strong>{manuscriptFile.name}</strong> ({Math.round(manuscriptFile.size/1024)} KB)</div>}
</div>
<div>
<label className="block text-sm mb-1">Figures (multiple)</label>
<input ref={figInputRef} type="file" accept="image/*,application/pdf" onChange={handleFiguresChange} multiple />
<div className="mt-2 grid grid-cols-2 md:grid-cols-4 gap-3">
{figPreviews.length === 0 && <div className="text-sm text-slate-500">No figures added</div>}
{figPreviews.map((p, i) => (
<div key={i} className="border rounded p-2 flex flex-col items-center text-center">
{p.url ? (
<img src={p.url} className="w-full h-28 object-contain mb-2" alt={p.name} />
) : (
<div className="w-full h-28 flex items-center justify-center text-xs text-slate-600 border rounded mb-2">{p.name}</div>
)}
<div className="text-xs">{p.name}</div>
<div className="flex gap-2 mt-2">
<button type="button" onClick={() => removeFigure(i)} className="text-xs btn-ghost">Remove</button>
</div>
</div>
))}
</div>
</div>
<div className="flex items-center gap-3">
<button type="submit" className="btn">Submit manuscript</button>
<button type="button" className="btn-ghost" onClick={() => { setTitle(""); setAbstract(""); setCoverLetter(""); setManuscriptFile(null); setFigureFiles([]); setFigPreviews([]); if (fileInputRef.current) fileInputRef.current.value = ""; if (figInputRef.current) figInputRef.current.value = ""; }}>Reset</button>
<div className="text-sm text-slate-600">{submitMessage}</div>
</div>
</form>
</div>
);
}
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<Nav />
<main className="py-8">
{view === "home" && <Home />}
{view === "register" && <RegisterView />}
{view === "login" && <LoginView />}
{view === "submit" && <SubmitView />}
</main>
<footer className="border-t mt-12 bg-white">
<div className="max-w-4xl mx-auto px-4 py-6 text-sm text-slate-600">Built with ♥ — replace demo submit handler with your API endpoint to accept real submissions.</div>
</footer>
{/* Tiny utility styles (could be moved to global stylesheet) */}
<style>{`
.input { width: 100%; padding: 0.6rem; border: 1px solid #E6E7EB; border-radius: 0.5rem; }
.textarea { width: 100%; padding: 0.6rem; border: 1px solid #E6E7EB; border-radius: 0.5rem; }
.btn { padding: 0.5rem 0.9rem; background: #0f172a; color: white; border-radius: 0.5rem; }
.btn-ghost { padding: 0.45rem 0.8rem; background: transparent; border-radius: 0.5rem; border: 1px solid transparent; }
.btn-outline { padding: 0.45rem 0.8rem; background: white; border-radius: 0.5rem; border: 1px solid #CBD5E1; }
.btn-ghost:hover, .btn:hover { opacity: 0.95; }
`}</style>
</div>
);
}
Untitled page