diff --git a/Images/365745efe57b55b82cba61e939dcec39.png b/Images/365745efe57b55b82cba61e939dcec39.png new file mode 100644 index 0000000..98d96ad Binary files /dev/null and b/Images/365745efe57b55b82cba61e939dcec39.png differ diff --git a/Images/IMG_0597_bright.jpeg b/Images/IMG_0597_bright.jpeg new file mode 100644 index 0000000..1dd0cc1 Binary files /dev/null and b/Images/IMG_0597_bright.jpeg differ diff --git a/Images/Podcast Art.jpeg b/Images/Podcast Art.jpeg new file mode 100644 index 0000000..6c62ee6 Binary files /dev/null and b/Images/Podcast Art.jpeg differ diff --git a/Images/Screenshot 2026-03-25 at 08-09-29 Verse by Verse with Nate.png b/Images/Screenshot 2026-03-25 at 08-09-29 Verse by Verse with Nate.png new file mode 100644 index 0000000..243e1e7 Binary files /dev/null and b/Images/Screenshot 2026-03-25 at 08-09-29 Verse by Verse with Nate.png differ diff --git a/Images/Study of Titus_ Sound Doctrine.png b/Images/Study of Titus_ Sound Doctrine.png new file mode 100644 index 0000000..a5ef4b9 Binary files /dev/null and b/Images/Study of Titus_ Sound Doctrine.png differ diff --git a/Images/Titus_Study_Cover.png b/Images/Titus_Study_Cover.png new file mode 100644 index 0000000..e01ad91 Binary files /dev/null and b/Images/Titus_Study_Cover.png differ diff --git a/Images/banner image.png b/Images/banner image.png new file mode 100644 index 0000000..79e4227 Binary files /dev/null and b/Images/banner image.png differ diff --git a/Images/banner_2048x1152.png b/Images/banner_2048x1152.png new file mode 100644 index 0000000..9811afb Binary files /dev/null and b/Images/banner_2048x1152.png differ diff --git a/Images/boookmark.docx b/Images/boookmark.docx new file mode 100644 index 0000000..c0d1ceb Binary files /dev/null and b/Images/boookmark.docx differ diff --git a/Images/boookmark.pdf b/Images/boookmark.pdf new file mode 100644 index 0000000..4d40347 Binary files /dev/null and b/Images/boookmark.pdf differ diff --git a/Images/listen-on-apple-podcasts-en-us/badge.svg b/Images/listen-on-apple-podcasts-en-us/badge.svg new file mode 100644 index 0000000..82a9273 --- /dev/null +++ b/Images/listen-on-apple-podcasts-en-us/badge.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Images/listen-on-apple-podcasts-en-us/mono-black.svg b/Images/listen-on-apple-podcasts-en-us/mono-black.svg new file mode 100644 index 0000000..54431da --- /dev/null +++ b/Images/listen-on-apple-podcasts-en-us/mono-black.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Images/listen-on-apple-podcasts-en-us/mono-white.svg b/Images/listen-on-apple-podcasts-en-us/mono-white.svg new file mode 100644 index 0000000..1924874 --- /dev/null +++ b/Images/listen-on-apple-podcasts-en-us/mono-white.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Images/listen-on-apple-podcasts-en-us/standard-black.svg b/Images/listen-on-apple-podcasts-en-us/standard-black.svg new file mode 100644 index 0000000..ac9b3b9 --- /dev/null +++ b/Images/listen-on-apple-podcasts-en-us/standard-black.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Images/listen-on-apple-podcasts-en-us/standard-white.svg b/Images/listen-on-apple-podcasts-en-us/standard-white.svg new file mode 100644 index 0000000..e0275fe --- /dev/null +++ b/Images/listen-on-apple-podcasts-en-us/standard-white.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Images/qr-code.jpg b/Images/qr-code.jpg new file mode 100644 index 0000000..8c2a5c4 Binary files /dev/null and b/Images/qr-code.jpg differ diff --git a/Images/verse-by-verse-qr-card.png b/Images/verse-by-verse-qr-card.png new file mode 100644 index 0000000..b8e1069 Binary files /dev/null and b/Images/verse-by-verse-qr-card.png differ diff --git a/Images/verse_by_verse_ad.html b/Images/verse_by_verse_ad.html new file mode 100644 index 0000000..6230205 --- /dev/null +++ b/Images/verse_by_verse_ad.html @@ -0,0 +1,72 @@ + + + + +Verse by Verse with Nate + + + +
+
+
+
+
+
+
+
+
✦   ✦   ✦
+
+
A Journey Through Scripture
+ VERSE + · by · + VERSE +
with Nate
+
+
+
+
+ QR Code +
+
Scan to Listen
+
+
+
+
Scripture · Reflection · Faith
+
+
+
+ + \ No newline at end of file diff --git a/data/admin-content.json b/data/admin-content.json index 8a0afc2..9580d96 100644 --- a/data/admin-content.json +++ b/data/admin-content.json @@ -1,382 +1,84 @@ { - "projects": [ + "siteContent": { + "eyebrow": "A Journey Through Scripture", + "heroTagline": "Exploring God's Word one verse at a time", + "aboutShowHeading": "Depth. Clarity. Application.", + "aboutShowP1": "Verse by Verse with Nate walks through Scripture passage by passage — unpacking the original context, drawing out the meaning, and connecting each verse to how we live today.", + "aboutShowP2": "Whether you're in the car, at the gym, or just looking for something to anchor your day, each episode is designed to feed your faith with solid, practical teaching.", + "aboutNate": "Nate Emmert is a husband, dad, and lifelong student of the Bible from Nashville, TN. He created Verse by Verse to share the joy of deep Scripture study in a format anyone can follow along with — no seminary required.", + "seriesLabel": "Now Playing", + "seriesTitle": "Study of Titus: Sound Doctrine", + "seriesDescription": "A deep-dive into Paul's letter to Titus — unpacking what it means to build a church and a life on sound doctrine.", + "studyGuideTitle": "Companion Study Guide", + "studyGuideDescription": "Go deeper in your study with the official Verse by Verse companion guide — now available on Amazon.", + "studyGuideUrl": "https://a.co/d/01sG2tOJ", + "shareHeading": "Know someone who'd love this?", + "shareP": "Scan the QR code or share the link to introduce a friend to Verse by Verse with Nate.", + "customLinks": [], + "customBlocks": [] + }, + "updatedAt": "2026-04-09T00:00:00.000Z", + "_legacy_projects": [ { - "slug": "weather", - "title": "NeCloud Weather", - "domain": "weather.necloud.us", - "url": "https://weather.necloud.us", - "readmeUrl": "https://raw.githubusercontent.com/nmemmert/weather/main/README.md", - "category": "Weather", + "slug": "verse-by-verse", + "title": "Verse by Verse with Nate", + "domain": "spotify.com", + "url": "https://creators.spotify.com/pod/profile/nmemmert/", + "category": "Podcast", "access": "Public", - "ownership": "built", + "ownership": "hosted", "status": "Live", - "summary": "Weather dashboard with hourly forecasts, radar, and practical daily details.", - "details": "The homepage highlights Open-Meteo and RainViewer data, current conditions, high/low, daylight windows, and quick bring-along suggestions.", + "summary": "A verse-by-verse podcast from Nate Emmert focused on Scripture, story, and practical faith application.", + "details": "Weekly episodes explore Scripture one verse at a time, mixing theological insight, devotional reflection, and real-life application.", "features": [ - "Hourly forecast", - "Radar view", - "Wind and atmosphere panel" - ], - "scanSource": "scanned", - "featured": { - "headline": "Fast weather checks with useful details at a glance.", - "problem": "Most weather tools either feel too basic or too noisy when you only want quick, actionable updates.", - "solution": "NeCloud Weather combines clean current conditions, practical recommendation blocks, and rapid forecast scanning in one view.", - "stack": [ - "React", - "TypeScript", - "Open-Meteo", - "RainViewer" - ], - "highlights": [ - "Location search", - "Hero weather card", - "24-hour timeline", - "Radar shortcut" - ], - "nextSteps": [ - "Add severe weather alerts", - "Save favorite locations", - "Add sunrise trend charts" - ], - "updateNote": "Edit this content in src/data/projects.ts under the weather project." - } - }, - { - "slug": "capsule", - "title": "Capsule", - "domain": "capsule.necloud.us", - "url": "https://capsule.necloud.us", - "readmeUrl": "https://raw.githubusercontent.com/nmemmert/capsule/main/README.md", - "category": "Finance", - "access": "Mixed", - "iconUrl": "https://capsule.necloud.us/images/capsule-logo.svg", - "ownership": "built", - "status": "Live", - "summary": "Smart envelope budgeting app focused on planning and income allocation.", - "details": "The site presents guided setup, envelope-based spending controls, and paycheck auto-allocation to keep budgets structured.", - "features": [ - "Envelope budgeting", - "Income auto-allocation", - "Offline-ready data" - ], - "scanSource": "scanned", - "featured": { - "headline": "A practical budgeting workflow built around envelope control.", - "problem": "Traditional budget tools can feel rigid or overwhelming for everyday spending decisions.", - "solution": "Capsule uses guided setup and smart allocation so users can organize spending with clear envelope categories.", - "stack": [ - "React", - "TypeScript", - "Local-first storage" - ], - "highlights": [ - "Guided setup wizard", - "Envelope categories", - "Auto-allocation rules" - ], - "nextSteps": [ - "Add recurring bill templates", - "Import transaction feeds", - "Monthly budget snapshots" - ], - "updateNote": "Edit this content in src/data/projects.ts under the capsule project." - } - }, - { - "slug": "nexusbible", - "title": "Nexus Bible", - "domain": "nexusbible.necloud.us", - "url": "https://nexusbible.necloud.us", - "readmeUrl": "https://raw.githubusercontent.com/nmemmert/nexusbible/main/README.md", - "category": "Study", - "access": "Mixed", - "ownership": "built", - "status": "Live", - "summary": "Bible reader for reading, comparison, and study using Free Use Bible API data.", - "details": "The app exposes navigation, reader controls, bookmarks, and daily-focus tooling to support regular scripture study.", - "features": [ - "Reader modes", - "Bookmarks", - "Daily focus workflow" - ], - "scanSource": "scanned", - "featured": { - "headline": "Scripture reading designed for depth, rhythm, and clarity.", - "problem": "Switching between apps for reading, comparison, and planning breaks study flow.", - "solution": "Nexus Bible unifies reader tools, bookmarks, and daily focus controls for a single focused study experience.", - "stack": [ - "React", - "TypeScript", - "Free Use Bible API" - ], - "highlights": [ - "Reader mode", - "Passage navigation", - "Bookmarks", - "Daily focus entry points" - ], - "nextSteps": [ - "Add multi-translation compare view", - "Personal note collections", - "Reading streak tracking" - ], - "updateNote": "Edit this content in src/data/projects.ts under the nexusbible project." - } - }, - { - "slug": "prayer", - "title": "Haver", - "domain": "prayer.necloud.us", - "url": "https://prayer.necloud.us", - "readmeUrl": "https://raw.githubusercontent.com/nmemmert/haver/main/README.md", - "category": "Faith", - "access": "Mixed", - "ownership": "built", - "status": "Live", - "summary": "Prayer journal app for daily entries, requests, and answered-prayer tracking.", - "details": "Landing content emphasizes a minimalist prayer workflow with dashboards, calendar history, and Google Calendar reminder sync.", - "features": [ - "Prayer journal", - "Prayer dashboard", - "Calendar integration" - ], - "scanSource": "scanned", - "featured": { - "headline": "A calm daily prayer workflow with meaningful tracking.", - "problem": "Prayer journaling often lives across scattered notes and reminders, making consistency difficult.", - "solution": "Haver centralizes requests, daily entries, answered prayers, and reminder sync into one simple interface.", - "stack": [ - "React", - "Firebase", - "Google Calendar integration" - ], - "highlights": [ - "Prayer composer", - "Answered prayer tracking", - "Calendar view", - "Reminder sync" - ], - "nextSteps": [ - "Add shared prayer groups", - "Voice prayer entry", - "Weekly summary digest" - ], - "updateNote": "Edit this content in src/data/projects.ts under the prayer project." - } - }, - { - "slug": "skywatch", - "title": "Skywatch", - "domain": "skywatch.necloud.us", - "url": "https://skywatch.necloud.us", - "category": "Sky and Space", - "access": "Public", - "ownership": "built", - "status": "Live", - "summary": "Skywatch project in your NeCloud stack.", - "details": "Live page content could not be scanned automatically because the endpoint responded with 401; this entry is currently a manual placeholder.", - "features": [ - "Manual metadata fallback", - "Live endpoint available via domain" + "Verse-by-verse teaching", + "Weekly podcast episodes", + "Practical reflection" ], "scanSource": "manual", "featured": { - "headline": "Skywatch project overview placeholder.", - "problem": "Automated scanning could not access this project because the endpoint returned 401.", - "solution": "This featured page is intentionally editable so you can replace placeholder copy with your exact app details.", + "headline": "Podcast teaching shaped around verse-by-verse discovery.", + "problem": "Bible study and podcast listening often feel disconnected from each other and from everyday life.", + "solution": "Verse by Verse with Nate brings Scripture study and podcast conversation together in a format that is easy to follow and apply.", "stack": [ - "Update manually" + "Podcast hosting", + "Spotify", + "Verse-by-verse teaching" ], "highlights": [ - "Placeholder content", - "Manual metadata fallback" + "Episode guides", + "Scripture focus", + "Practical application" ], "nextSteps": [ - "Add full description", - "Add stack details", - "Add feature highlights" + "Add episode notes", + "Add guest interviews", + "Link transcripts and study resources" ], - "updateNote": "Edit this content in src/data/projects.ts under the skywatch project." - } - }, - { - "slug": "spellinghub", - "title": "Spelling Hub", - "domain": "spellinghub.necloud.us", - "url": "https://spellinghub.necloud.us", - "readmeUrl": "https://raw.githubusercontent.com/nmemmert/spellinghub/main/README.md", - "category": "Education", - "access": "Mixed", - "ownership": "built", - "status": "Live", - "summary": "Modern spelling practice application with account-based learning flows.", - "details": "Homepage messaging presents a clean practice platform with login and registration for user progress.", - "features": [ - "Practice workflows", - "User auth", - "Simple home dashboard" - ], - "scanSource": "scanned", - "featured": { - "headline": "Focused spelling practice with lightweight account flow.", - "problem": "Many spelling tools are cluttered and do not support structured, repeatable daily practice.", - "solution": "Spelling Hub keeps onboarding simple and centers the product on practical, repeatable learning loops.", - "stack": [ - "React", - "TypeScript", - "Auth-backed workflow" - ], - "highlights": [ - "Login/register flow", - "Practice-first design", - "Simple navigation" - ], - "nextSteps": [ - "Add custom word lists", - "Teacher/student modes", - "Progress analytics dashboard" - ], - "updateNote": "Edit this content in src/data/projects.ts under the spellinghub project." - } - }, - { - "slug": "mealie", - "title": "Mealie", - "domain": "mealie.necloud.us", - "url": "https://mealie.necloud.us", - "category": "Self-hosted Tools", - "access": "Mixed", - "ownership": "hosted", - "status": "Live", - "summary": "Self-hosted recipe manager with categories, tags, and kitchen planning tools.", - "details": "Scanned content confirms a full Mealie interface with home feed, recipe browsing, and account login flow.", - "features": [ - "Recipe library", - "Tags and categories", - "User login" - ], - "scanSource": "scanned", - "featured": { - "headline": "Recipe organization and planning through your self-hosted stack.", - "problem": "Recipe collections get scattered across browser bookmarks and notes without centralized organization.", - "solution": "Your Mealie instance provides structured categories, tags, and searchable recipes for day-to-day meal planning.", - "stack": [ - "Mealie", - "Self-hosted deployment" - ], - "highlights": [ - "Recipe catalog", - "Category and tag views", - "Account login" - ], - "nextSteps": [ - "Add meal plan snapshots", - "Import bookmark recipes", - "Shopping list workflows" - ], - "updateNote": "Edit this content in src/data/projects.ts under the mealie project." - } - }, - { - "slug": "music", - "title": "Swing Music", - "domain": "music.necloud.us", - "url": "https://music.necloud.us", - "category": "Self-hosted Tools", - "access": "Mixed", - "ownership": "hosted", - "status": "Live", - "summary": "Self-hosted music streaming interface with folders, playlists, and favorites.", - "details": "Homepage scan identifies Swing Music with library navigation, playlists, favorites, and guest/standard access options.", - "features": [ - "Library browsing", - "Playlists", - "Guest mode" - ], - "scanSource": "scanned", - "featured": { - "headline": "Swing Music streaming from your own infrastructure.", - "problem": "Centralized music services can limit control over personal libraries and hosting preferences.", - "solution": "Your Swing Music instance provides library browsing, playlists, and favorites with local hosting control.", - "stack": [ - "Swing Music", - "Self-hosted deployment" - ], - "highlights": [ - "Library navigation", - "Playlist support", - "Favorites", - "Guest mode" - ], - "nextSteps": [ - "Improve mobile now-playing UI", - "Add listening history", - "Playlist sharing links" - ], - "updateNote": "Edit this content in src/data/projects.ts under the music project." - } - }, - { - "slug": "seafile", - "title": "Seafile Instance", - "domain": "seafile.necloud.us", - "url": "https://seafile.necloud.us", - "category": "Self-hosted Tools", - "access": "Login required", - "iconUrl": "https://seafile.necloud.us/media/custom/mylogo.png", - "ownership": "hosted", - "status": "Live", - "summary": "Your self-hosted Seafile instance for secure sync, sharing, and account-based access.", - "details": "Scanned landing view exposes the Seafile login portal with account signup and password reset features.", - "features": [ - "File sync platform", - "Account login", - "Password recovery" - ], - "scanSource": "scanned", - "featured": { - "headline": "A private file sync and sharing system you host yourself.", - "problem": "Cloud storage options often reduce control over data location and account governance.", - "solution": "Your Seafile instance gives you direct ownership of file sync, sharing access, and user account management.", - "stack": [ - "Seafile", - "Self-hosted deployment" - ], - "highlights": [ - "Secure login", - "Account management", - "Password reset flow" - ], - "nextSteps": [ - "Add team spaces", - "Automated backup checks", - "Storage usage dashboard" - ], - "updateNote": "Edit this content in src/data/projects.ts under the seafile project." + "updateNote": "Edit this content in data/admin-content.json for the Verse by Verse podcast project." } } ], "siteContent": { "theme": "sand", - "homeEyebrow": "Siteforge", - "homeTitle": "Sites and Apps I Build", - "homeIntro": "Live links generated from your domain list. Use the Admin panel to update cards and About pages without editing code.", - "projectEyebrow": "Project Details", - "quickTipsTitle": "Quick edit tips", - "quickTipsBody": "Open /admin to update card and featured page content. Changes are saved in your project data file via the API server.", - "downloadsTitle": "Downloads", - "downloadsIntro": "Add shared files, installers, or documents here. Seafile share links work well for this section.", + "homeEyebrow": "Verse by Verse with Nate", + "homeTitle": "Podcast episodes, Scripture study, and weekly reflections", + "homeIntro": "A podcast experience centered on verse-by-verse conversation, prayerful application, and practical faith rhythms. Explore episodes, show notes, and the latest reflections from Nate.", + "projectEyebrow": "Featured Episode", + "quickTipsTitle": "Why listeners stay engaged", + "quickTipsBody": "Each episode focuses on clear Scripture teaching, practical application, and encouragement you can carry into the week.", + "downloadsTitle": "Listen Everywhere", + "downloadsIntro": "Use these links to follow the show, share episodes, and stay connected with Verse by Verse with Nate.", "downloads": [ { - "title": "Shared File", - "url": "https://seafile.necloud.us", - "description": "Replace this with a direct Seafile share link or another download URL." + "title": "Spotify Creator Profile", + "url": "https://creators.spotify.com/pod/profile/nmemmert/", + "description": "Open the official show profile and listen to the latest episodes." } ], - "showSidebar": true, - "showWeatherWidget": true, - "showMusicBar": true, + "showSidebar": false, + "showWeatherWidget": false, + "showMusicBar": false, "homeSections": [ { "id": "summary", @@ -387,14 +89,14 @@ "placement": "main" }, { - "id": "downloads", - "placement": "sidebar" + "id": "quickTips", + "placement": "main" }, { - "id": "quickTips", - "placement": "sidebar" + "id": "downloads", + "placement": "main" } ] }, - "updatedAt": "2026-03-19T19:19:04.291Z" + "updatedAt": "2026-04-09T00:00:00.000Z" } \ No newline at end of file diff --git a/index.html b/index.html index c9b373c..e4b1551 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,24 @@ - Siteforge + + Verse by Verse with Nate + + + + + + + + + + + + + + + +
diff --git a/public/images/apple-podcasts-badge.svg b/public/images/apple-podcasts-badge.svg new file mode 100644 index 0000000..e0275fe --- /dev/null +++ b/public/images/apple-podcasts-badge.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/banner.png b/public/images/banner.png new file mode 100644 index 0000000..9811afb Binary files /dev/null and b/public/images/banner.png differ diff --git a/public/images/nate-photo.jpeg b/public/images/nate-photo.jpeg new file mode 100644 index 0000000..1dd0cc1 Binary files /dev/null and b/public/images/nate-photo.jpeg differ diff --git a/public/images/podcast-art.jpeg b/public/images/podcast-art.jpeg new file mode 100644 index 0000000..6c62ee6 Binary files /dev/null and b/public/images/podcast-art.jpeg differ diff --git a/public/images/qr-card.png b/public/images/qr-card.png new file mode 100644 index 0000000..b8e1069 Binary files /dev/null and b/public/images/qr-card.png differ diff --git a/public/images/qr-code.jpg b/public/images/qr-code.jpg new file mode 100644 index 0000000..8c2a5c4 Binary files /dev/null and b/public/images/qr-code.jpg differ diff --git a/public/images/titus-cover.png b/public/images/titus-cover.png new file mode 100644 index 0000000..e01ad91 Binary files /dev/null and b/public/images/titus-cover.png differ diff --git a/server.js b/server.js index 2fba5b0..91c3f88 100644 --- a/server.js +++ b/server.js @@ -25,17 +25,17 @@ app.get('/api/admin-content', async (_req, res) => { app.put('/api/admin-content', async (req, res) => { try { - const { projects, siteContent } = req.body ?? {} + const { siteContent } = req.body ?? {} - if (!Array.isArray(projects) || !siteContent || typeof siteContent !== 'object') { - res.status(400).json({ message: 'Invalid payload.' }) + if (!siteContent || typeof siteContent !== 'object' || Array.isArray(siteContent)) { + res.status(400).json({ message: 'Invalid payload: siteContent must be an object.' }) return } await mkdir(DATA_DIR, { recursive: true }) await writeFile( DATA_FILE, - JSON.stringify({ projects, siteContent, updatedAt: new Date().toISOString() }, null, 2), + JSON.stringify({ siteContent, updatedAt: new Date().toISOString() }, null, 2), 'utf8', ) diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx new file mode 100644 index 0000000..4395d15 --- /dev/null +++ b/src/AdminPage.tsx @@ -0,0 +1,260 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import type { SiteContent, CustomLink, CustomBlock } from './App' +import { DEFAULTS } from './App' + +interface Props { + content: SiteContent + onSave: (c: SiteContent) => void +} + +type StringField = Exclude + +const FIELDS: { key: StringField; label: string; multiline?: boolean }[] = [ + { key: 'eyebrow', label: 'Hero Eyebrow Text' }, + { key: 'heroTagline', label: 'Hero Tagline' }, + { key: 'aboutShowHeading', label: 'About Show — Heading' }, + { key: 'aboutShowP1', label: 'About Show — Paragraph 1', multiline: true }, + { key: 'aboutShowP2', label: 'About Show — Paragraph 2', multiline: true }, + { key: 'aboutNate', label: 'About Nate', multiline: true }, + { key: 'seriesLabel', label: 'Series Label (e.g. "Now Playing")' }, + { key: 'seriesTitle', label: 'Series Title' }, + { key: 'seriesDescription', label: 'Series Description', multiline: true }, + { key: 'studyGuideTitle', label: 'Study Guide Title' }, + { key: 'studyGuideDescription', label: 'Study Guide Description', multiline: true }, + { key: 'studyGuideUrl', label: 'Study Guide URL (Amazon link)' }, + { key: 'shareHeading', label: 'Share Section — Heading' }, + { key: 'shareP', label: 'Share Section — Paragraph', multiline: true }, +] + +export default function AdminPage({ content, onSave }: Props) { + const [form, setForm] = useState(content) + const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') + const [errorMsg, setErrorMsg] = useState('') + + function handleChange(key: StringField, value: string) { + setForm(f => ({ ...f, [key]: value })) + } + + function addLink() { + setForm(f => ({ + ...f, + customLinks: [ + ...(f.customLinks ?? []), + { id: Date.now().toString(36), label: '', url: '', placement: 'platforms' as const }, + ], + })) + } + + function updateLink(id: string, field: keyof CustomLink, value: string) { + setForm(f => ({ + ...f, + customLinks: (f.customLinks ?? []).map(l => l.id === id ? { ...l, [field]: value } : l), + })) + } + + function removeLink(id: string) { + setForm(f => ({ ...f, customLinks: (f.customLinks ?? []).filter(l => l.id !== id) })) + } + + function addBlock() { + setForm(f => ({ + ...f, + customBlocks: [ + ...(f.customBlocks ?? []), + { id: Date.now().toString(36), heading: '', body: '' }, + ], + })) + } + + function updateBlock(id: string, field: keyof CustomBlock, value: string) { + setForm(f => ({ + ...f, + customBlocks: (f.customBlocks ?? []).map(b => b.id === id ? { ...b, [field]: value } : b), + })) + } + + function removeBlock(id: string) { + setForm(f => ({ ...f, customBlocks: (f.customBlocks ?? []).filter(b => b.id !== id) })) + } + + async function handleSave() { + setStatus('saving') + setErrorMsg('') + try { + const res = await fetch('/api/admin-content', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ siteContent: form }), + }) + if (!res.ok) { + const data = await res.json().catch(() => ({})) + throw new Error((data as { message?: string }).message ?? 'Save failed') + } + onSave(form) + setStatus('saved') + setTimeout(() => setStatus('idle'), 3500) + } catch (err) { + setErrorMsg(err instanceof Error ? err.message : 'Unknown error') + setStatus('error') + } + } + + function handleReset() { + if (confirm('Reset all fields to defaults?')) { + setForm(DEFAULTS) + setStatus('idle') + } + } + + return ( +
+
+ ✦   ✦   ✦ +

Site Admin

+

Verse by Verse with Nate

+ ← Back to site +
+ +
+
{ e.preventDefault(); handleSave() }} + > + {FIELDS.map(({ key, label, multiline }) => ( +
+ + {multiline ? ( +