// ============================================================ // Mobile app v2 — orchestrator, nav, data // Loads LAST — after all screen files // ============================================================ const SEED_POSTS_V2 = [ { id: 1, title: "The Last Loom Master of Kanchipuram", date: "7 May 2026", views: "8.3k", cat: "Cultural Wealth", status: "APPROVED", location: "Kanchipuram, TN", hasMedia: true, body: "At 74, Ramanathan still throws the shuttle twelve hours a day, weaving the temple-border technique his family has kept for eleven generations." }, { id: 2, title: "Water shortage: Mettupalayam residents protest", date: "20 May 2026", views: "6.1k", cat: "Water", status: "UNDER REVIEW", location: "Mettupalayam, TN", hasMedia: true, body: "Three wards have relied on tanker deliveries since March. Residents blocked the bypass road for four hours demanding restored piped supply." }, { id: 3, title: "How a Small Library in Coimbatore Rewrote Its Community", date: "9 May 2026", views: "2.4k", cat: "Civic Infrastructure", status: "CHANGES REQ.", location: "Coimbatore, TN", hasMedia: false, note: "Strong piece — please verify the 400-books-a-week figure with a source, and fix the date in the second paragraph before we publish.", body: "When the municipal library shut in 2021, a group of retired teachers reopened it from a converted garage. Three years on it lends 400 books a week." }, { id: 4, title: "Night markets after the floods: Velachery finds its feet", date: "2 May 2026", views: "—", cat: "Civic Infrastructure", status: "DRAFT", location: "Chennai, TN", hasMedia: false, body: "Vendors describe a slow return of customers to the evening market, six weeks after the water receded." }, { id: 5, title: "A power grid stretched thin in the Nilgiris", date: "28 Apr 2026", views: "1.2k", cat: "Power", status: "HELD", location: "Ooty, TN", hasMedia: true, body: "Outages that once lasted minutes now stretch to hours as tourist-season demand collides with ageing hill-station infrastructure." }, ]; const SEED_NOTIFS_V2 = [ { id: 1, type: "breaking", unread: true, time: "30m ago", text: "Breaking: Heavy rains disrupt traffic across Chennai — Read now" }, { id: 2, type: "changes", postId: 3, unread: true, time: "2h ago", text: "Editor requested changes on How a Small Library in Coimbatore…" }, { id: 3, type: "message", unread: true, time: "3h ago", text: "Editor Priya: \"Great work on the Kanchipuram piece. Would you consider a follow-up?\"" }, { id: 4, type: "deadline", postId: 3, unread: true, time: "5h ago", text: "Reminder: edits on Coimbatore Library are due by end of day." }, { id: 5, type: "published", postId: 1, unread: true, time: "1d ago", text: "The Last Loom Master of Kanchipuram is now live on nambikkai.com" }, { id: 6, type: "milestone", postId: 1, unread: false, time: "12h ago", text: "The Last Loom Master… crossed 5,000 reads!" }, { id: 7, type: "approved", postId: 1, unread: false, time: "1d ago", text: "Your post The Last Loom Master of Kanchipuram was approved." }, { id: 8, type: "review", postId: 2, unread: false, time: "2d ago", text: "Water shortage: Mettupalayam… is now under editorial review." }, { id: 9, type: "article", unread: false, time: "6h ago", text: "New on Nambikkai: Salem's Steel Town Reckons With Quiet Furnaces" }, { id: 10, type: "held", postId: 5, unread: false, time: "4d ago", text: "A power grid stretched thin… was held pending verification." }, ]; // ============================ TOP BAR ============================ function PhoneTopBar({ dark, onToggleDark, unread, onBell }) { return (
{ e.currentTarget.onerror = null; e.currentTarget.src = "assets/logo.png"; }} />
{APP_NAME_V2}
நம்பிக்கை · v1.0
); } // ============================ BOTTOM NAV ============================ function BottomNav({ active, onTab }) { const items = [ { k: "Profile", label: "Profile", icon: Icons.user }, { k: "Home", label: "Home", icon: Icons.home, primary: true }, { k: "Posts", label: "Posts", icon: Icons.news }, ]; return (
{items.map((it) => { const on = it.k === active; const Ic = it.icon; const tint = on ? "var(--blue)" : "var(--subtext)"; return ( ); })}
); } // ============================ MAIN APP ============================ function MobileApp() { const [dark, setDark] = React.useState(() => localStorage.getItem("nk-dark") === "1"); React.useEffect(() => { document.documentElement.classList.toggle("dark", dark); localStorage.setItem("nk-dark", dark ? "1" : "0"); }, [dark]); const [authed, setAuthed] = React.useState(false); const [tab, setTab] = React.useState("Profile"); const [posts, setPosts] = React.useState(SEED_POSTS_V2); const [notifs, setNotifs] = React.useState(SEED_NOTIFS_V2); const [editing, setEditing] = React.useState(null); const [editProfile, setEditProfile] = React.useState(false); const [notifOpen, setNotifOpen] = React.useState(false); const [settingsOpen, setSettingsOpen] = React.useState(false); const [settings, setSettings] = React.useState({ statusChange: true, editorMsg: true, milestones: false, deadlines: true, breaking: true, newArticles: false, editorPicks: false, }); const [modal, setModal] = React.useState(null); const [toast, setToast] = React.useState(null); const [toastTone, setToastTone] = React.useState("success"); const [terms, setTerms] = React.useState(null); const dirtyRef = React.useRef(false); const unread = notifs.filter((n) => n.unread).length; const showToast = (m, tone = "success") => { setToast(m); setToastTone(tone); window.clearTimeout(showToast._t); showToast._t = window.setTimeout(() => setToast(null), 2300); }; // ---- navigation with discard guard ---- const go = (to) => { if (tab === "Report" && dirtyRef.current) { setModal({ title: "Discard changes?", body: "You have unsaved edits to this report. Leaving now will lose them.", tone: "amber", icon: Icons.alert, confirmLabel: "Discard", cancelLabel: "Keep editing", onConfirm: () => { dirtyRef.current = false; setEditing(null); setTab(to); } }); return; } if (tab === "Report") setEditing(null); setEditProfile(false); setTab(to); }; const startNew = () => { dirtyRef.current = false; setEditing({}); setTab("Report"); }; const startEdit = (postId) => { const p = posts.find((x) => x.id === postId); if (!p) return; setNotifOpen(false); dirtyRef.current = false; setEditing(p); setTab("Report"); }; const upsert = (payload, status) => { dirtyRef.current = false; setPosts((ps) => { if (editing && editing.id != null) { return ps.map((p) => p.id === editing.id ? { ...p, ...payload, status } : p); } const id = Math.max(0, ...ps.map((p) => p.id)) + 1; return [{ id, date: "Today", views: "—", hasMedia: false, ...payload, status }, ...ps]; }); setEditing(null); setTab("Posts"); }; const handleSaveDraft = (payload) => { if (!payload.title) { showToast("Add a headline to save", "amber"); return; } setModal({ title: "Save as draft?", body: "Your report will be saved locally. You can resume editing it anytime from your drafts.", tone: "blue", icon: Icons.save, confirmLabel: "Save draft", cancelLabel: "Cancel", onConfirm: () => { upsert(payload, "DRAFT"); showToast("Draft saved"); } }); }; const handleSubmit = (payload) => { const isResubmit = editing && editing.id != null && editing.status !== "DRAFT"; setModal({ title: isResubmit ? "Resubmit article?" : "Submit for review?", body: isResubmit ? "This will send your updated article back to the editorial desk for re-review." : "Your report will be sent to the editorial desk for review and verification.", tone: "blue", icon: Icons.send, confirmLabel: isResubmit ? "Resubmit" : "Submit", cancelLabel: "Cancel", onConfirm: () => setTerms({ payload, resubmit: isResubmit }) }); }; const acceptTerms = () => { if (!terms) return; const { payload, resubmit } = terms; upsert(payload, "UNDER REVIEW"); setTerms(null); showToast(resubmit ? "Resubmitted for review" : "Submitted for review"); }; const handleDelete = (post) => { setModal({ title: post.status === "DRAFT" ? "Delete this draft?" : "Delete this article?", body: "This permanently removes it from your posts. This can't be undone.", tone: "red", icon: Icons.trash, confirmLabel: "Delete", onConfirm: () => { setPosts((ps) => ps.filter((p) => p.id !== post.id)); dirtyRef.current = false; setEditing(null); setTab("Posts"); showToast("Article deleted"); }, }); }; const openNotifItem = (n) => { setNotifs((ns) => ns.map((x) => x.id === n.id ? { ...x, unread: false } : x)); if ((n.type === "changes" || n.type === "deadline") && n.postId != null) { startEdit(n.postId); } else { setNotifOpen(false); if (n.type === "breaking" || n.type === "article") { setTab("Home"); } else { setTab("Posts"); } } }; const doLogout = () => { setModal({ title: "Log out?", body: "You'll need to sign in again to file or track reports.", tone: "amber", icon: Icons.logout, confirmLabel: "Log out", onConfirm: () => { setAuthed(false); setTab("Home"); setEditing(null); setEditProfile(false); } }); }; const login = (method) => { setAuthed(true); setTab("Profile"); showToast(method === "google" ? "Signed in with Google" : method === "signup" ? "Account created — welcome!" : "Welcome back"); }; // edit profile view is a sub-screen of Profile tab const showProfile = tab === "Profile" && !editProfile; return (
{!authed ? ( ) : ( setDark((d) => !d)} unread={unread} onBell={() => setNotifOpen(true)} />
{tab === "Report" && ( { dirtyRef.current = d; }} /> )} {tab === "Posts" && } {tab === "Profile" && editProfile && ( setEditProfile(false)} showToast={showToast} /> )} {showProfile && ( setNotifOpen(true)} onOpenNotifSettings={() => setSettingsOpen(true)} onEditProfile={() => setEditProfile(true)} onLogout={doLogout} /> )} {tab === "Home" && }
)} {/* overlays */} {authed && notifOpen && ( setNotifOpen(false)} onOpenItem={openNotifItem} /> )} {authed && settingsOpen && ( setSettingsOpen(false)} onChange={(k, v) => setSettings((s) => ({ ...s, [k]: v }))} /> )} {authed && terms && ( setTerms(null)} /> )} setModal(null)} />
); } ReactDOM.createRoot(document.getElementById("root")).render();