// ============================================================
// Mobile — Report (with auto-save), Posts, Home/Site, Terms
// Loads after profile-v2.jsx
// ============================================================
const CATS_V2 = ["Roads", "Water", "Power", "Safety", "Civic Infrastructure", "Cultural Wealth", "Other"];
// ============================ REPORT SCREEN ============================
function ReportScreen({ editing, onSaveDraft, onSubmit, onDelete, onDirty }) {
const isEdit = editing && editing.id != null;
const [lang, setLang] = React.useState("en");
const [title, setTitle] = React.useState(editing?.title || "");
const [desc, setDesc] = React.useState(editing?.body || "");
const [cat, setCat] = React.useState(editing?.cat || "Roads");
const [loc, setLoc] = React.useState(editing?.location || "Kanchipuram, TN");
const [dirty, setDirtyLocal] = React.useState(false);
const [autoSaved, setAutoSaved] = React.useState(null);
const [saving, setSaving] = React.useState(false);
const initRef = React.useRef(JSON.stringify({
title: editing?.title || "", desc: editing?.body || "",
cat: editing?.cat || "Roads", loc: editing?.location || "Kanchipuram, TN",
}));
const autoSaveTimer = React.useRef(null);
React.useEffect(() => {
const isDirty = JSON.stringify({ title, desc, cat, loc }) !== initRef.current;
setDirtyLocal(isDirty);
onDirty && onDirty(isDirty);
// auto-save after 3 seconds of inactivity
if (isDirty && title.trim().length > 3) {
clearTimeout(autoSaveTimer.current);
autoSaveTimer.current = setTimeout(() => {
setSaving(true);
setTimeout(() => {
setSaving(false);
const now = new Date();
setAutoSaved(now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
}, 600);
}, 3000);
}
return () => clearTimeout(autoSaveTimer.current);
}, [title, desc, cat, loc]);
const payload = () => ({ title: title.trim(), body: desc.trim(), cat, location: loc.trim() });
const canSubmit = title.trim().length > 3 && desc.trim().length > 8;
const L = {
en: { head: "Headline", desc: "Description", cat: "Category", loc: "Location",
hph: "What happened?", dph: "Describe the situation on the ground…" },
ta: { head: "தலைப்பு", desc: "விவரம்", cat: "வகை", loc: "இடம்",
hph: "என்ன நடந்தது?", dph: "நிலைமையை விளக்குங்கள்…" },
}[lang];
return (
{/* header + auto-save */}
{isEdit ? "Edit article" : "Report a Story"}
{[["ta", "தமிழ்"], ["en", "EN"]].map(([k, l]) => (
setLang(k)} style={{ padding: "5px 11px", borderRadius: 7,
fontSize: 12.5, fontWeight: 600, fontFamily: "var(--ff-ui)", border: "none",
cursor: "pointer", transition: "all .15s ease",
color: lang === k ? "#fff" : "var(--subtext)",
background: lang === k ? "var(--blue)" : "transparent" }}>{l}
))}
{/* requested-changes banner */}
{isEdit && editing.status === "CHANGES REQ." && editing.note && (
Editor requested changes
{editing.note}
)}
setTitle(e.target.value)}
style={nkInput} placeholder={L.hph} />
{CATS_V2.map((c) => (
setCat(c)} mono={false}>{c}
))}
setLoc(e.target.value)}
style={{ ...nkInput, paddingLeft: 38 }} />
{/* media upload */}
{editing?.hasMedia ? "1 photo attached" : "Add photo or video"}
Min. 720p · Max. 50 MB
{/* actions */}
onSaveDraft(payload())} style={{ flex: "0 0 auto",
padding: "13px 16px", borderRadius: 11, background: "var(--bg-card)",
color: "var(--text-2)", border: "1px solid var(--border-2)",
fontFamily: "var(--ff-ui)", fontWeight: 600, fontSize: 14.5,
cursor: "pointer", display: "flex", alignItems: "center", gap: 7 }}>
Save draft
canSubmit && onSubmit(payload())} disabled={!canSubmit}
style={{ flex: 1, padding: "13px", borderRadius: 11,
fontFamily: "var(--ff-ui)", fontWeight: 600, fontSize: 15,
color: "#fff", background: canSubmit ? "var(--blue)" : "var(--border-2)",
border: "none", cursor: canSubmit ? "pointer" : "not-allowed",
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
boxShadow: canSubmit ? "0 6px 16px rgba(0,68,187,0.28)" : "none",
transition: "all .15s ease" }}>
{isEdit && editing.status !== "DRAFT" ? "Resubmit" : "Submit report"}
{isEdit && (
onDelete(editing)} style={{ width: "100%", marginTop: 14,
padding: "11px", borderRadius: 11, background: "none", color: "var(--red)",
border: "none", fontFamily: "var(--ff-ui)", fontWeight: 600, fontSize: 14,
cursor: "pointer", display: "flex", alignItems: "center",
justifyContent: "center", gap: 7 }}>
Delete {editing.status === "DRAFT" ? "draft" : "article"}
)}
);
}
const FormField = ({ label, children }) => (
{label}
{children}
);
// ============================ POSTS ============================
const POST_FILTERS_V2 = [
{ label: "All", match: () => true },
{ label: "Published", match: (s) => s.status === "APPROVED" },
{ label: "In review", match: (s) => s.status === "UNDER REVIEW" },
{ label: "Needs edit", match: (s) => s.status === "CHANGES REQ." },
{ label: "Held", match: (s) => s.status === "HELD" },
{ label: "Drafts", match: (s) => s.status === "DRAFT" },
];
const EDITABLE_V2 = ["DRAFT", "CHANGES REQ."];
function PostsScreen({ posts, onEdit, onNew }) {
const [fi, setFi] = React.useState(0);
const list = posts.filter(POST_FILTERS_V2[fi].match);
return (
Your posts
{posts.length} total
New report
{POST_FILTERS_V2.map((f, i) => (
setFi(i)}
mono={false}>{f.label}
))}
{list.map((s) => {
const editable = EDITABLE_V2.includes(s.status);
return (
editable && onEdit(s.id)}
style={{ background: "var(--bg-card)", borderRadius: 12,
border: "1px solid var(--border)", padding: 13,
boxShadow: "var(--shadow-sm)",
cursor: editable ? "pointer" : "default",
transition: "border-color .15s ease" }}>
{s.date}
{s.views} views
{editable ? (
{s.status === "CHANGES REQ." ? "Fix & resubmit" : "Edit"}
) : (
{s.cat}
)}
);
})}
{list.length === 0 && (
No posts here yet
Posts matching this filter will appear here.
)}
);
}
// ============================ HOME / SITE ============================
function HomeSiteScreen() {
const URL = "https://www.nambikkai.com";
const [nonce, setNonce] = React.useState(0);
const [loaded, setLoaded] = React.useState(false);
return (
nambikkai.com
{ setLoaded(false); setNonce((n) => n + 1); }}
style={{ color: "var(--subtext)", background: "none", border: "none",
cursor: "pointer", padding: 4 }}>
);
}
// ============================ TERMS & CONDITIONS ============================
const TERMS_V2 = [
["1 · Accuracy & good faith",
"I confirm the report is true to the best of my knowledge, based on what I witnessed or verified, and is not knowingly false, misleading, or fabricated."],
["2 · Original work & rights",
"The text, photos, and video I submit are my own original work, or I hold the rights to share them. I grant Nambikkai a licence to review, edit, publish, and distribute this content across its platforms."],
["3 · No harmful content",
"My submission contains no hate speech, harassment, defamation, graphic content without public-interest value, or material that endangers any individual's safety or privacy."],
["4 · Editorial review",
"I understand every submission is reviewed by the editorial desk, may be edited for clarity, fact-checked, held, or declined, and that submitting does not guarantee publication."],
["5 · Location & personal data",
"I consent to the location and contributor details attached to this report being used for verification. I have not included others' personal data without their consent."],
];
function TermsSheet({ resubmit, onAgree, onClose }) {
const [agreed, setAgreed] = React.useState(false);
return (
e.stopPropagation()} style={{ background: "var(--bg-app)",
borderTopLeftRadius: 22, borderTopRightRadius: 22, maxHeight: "86%",
display: "flex", flexDirection: "column",
animation: "nk-slideup .26s cubic-bezier(.2,.8,.2,1)" }}>
Contributor agreement
Please review before {resubmit ? "resubmitting" : "submitting"} to the editorial desk.
{TERMS_V2.map(([h, b]) => (
))}
setAgreed((a) => !a)} style={{ width: "100%", display: "flex",
alignItems: "center", gap: 11, background: "none", border: "none",
cursor: "pointer", padding: "4px 0 14px", textAlign: "left" }}>
{agreed && }
I have read and agree to the contributor agreement.
Cancel
agreed && onAgree()} disabled={!agreed}
style={{ flex: 1, padding: "13px", borderRadius: 11,
fontFamily: "var(--ff-ui)", fontWeight: 600, fontSize: 15,
color: "#fff", background: agreed ? "var(--blue)" : "var(--border-2)",
border: "none", cursor: agreed ? "pointer" : "not-allowed",
display: "flex", alignItems: "center", justifyContent: "center", gap: 8 }}>
{resubmit ? "Agree & resubmit" : "Agree & post"}
);
}
Object.assign(window, { ReportScreen, PostsScreen, HomeSiteScreen, TermsSheet, CATS_V2 });