adding
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
{
|
||||
"checklist": {
|
||||
"tasks": [
|
||||
{
|
||||
"id": "verify_script",
|
||||
"label": "Verify Script",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "read_script",
|
||||
"label": "Read Script",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "record",
|
||||
"label": "Record",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "edit",
|
||||
"label": "Edit",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "mix",
|
||||
"label": "Mix",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "video_script",
|
||||
"label": "Run Video Conversion Script",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "post_spotify",
|
||||
"label": "Post on Spotify",
|
||||
"phase": "pre"
|
||||
},
|
||||
{
|
||||
"id": "update_website",
|
||||
"label": "Update Website",
|
||||
"phase": "post"
|
||||
},
|
||||
{
|
||||
"id": "send_email",
|
||||
"label": "Send Email",
|
||||
"phase": "post"
|
||||
}
|
||||
],
|
||||
"episodes": [
|
||||
{
|
||||
"id": "titus-11",
|
||||
"series": "Titus",
|
||||
"episodeNumber": 11,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "titus-12",
|
||||
"series": "Titus",
|
||||
"episodeNumber": 12,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "titus-13",
|
||||
"series": "Titus",
|
||||
"episodeNumber": 13,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "titus-14",
|
||||
"series": "Titus",
|
||||
"episodeNumber": 14,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "titus-15",
|
||||
"series": "Titus",
|
||||
"episodeNumber": 15,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-1",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 1,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-2",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 2,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-3",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 3,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-4",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 4,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-5",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 5,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-6",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 6,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-7",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 7,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-8",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 8,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-9",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 9,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-10",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 10,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-11",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 11,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-12",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 12,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-13",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 13,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-14",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 14,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-15",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 15,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-16",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 16,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-17",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 17,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-18",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 18,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-19",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 19,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-20",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 20,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-21",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 21,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-22",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 22,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-23",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 23,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-24",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 24,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-25",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 25,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-26",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 26,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "colossians-27",
|
||||
"series": "Colossians",
|
||||
"episodeNumber": 27,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "episode-mpb66z1c",
|
||||
"series": "Titus -10",
|
||||
"episodeNumber": 0,
|
||||
"title": "",
|
||||
"datePublished": "",
|
||||
"expanded": false,
|
||||
"tasks": {
|
||||
"verify_script": false,
|
||||
"read_script": false,
|
||||
"record": false,
|
||||
"edit": false,
|
||||
"mix": false,
|
||||
"video_script": false,
|
||||
"post_spotify": false,
|
||||
"update_website": false,
|
||||
"send_email": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"updatedAt": "2026-05-18T12:18:35.130Z"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install --production
|
||||
|
||||
COPY server.js ./
|
||||
COPY public/ ./public/
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENV PORT=3000
|
||||
ENV DATA_FILE=/data/progress.json
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Podcast Checklist
|
||||
|
||||
Episode production tracker for *Verse by Verse with Nate*.
|
||||
|
||||
## Deploy
|
||||
|
||||
### 1. Build and push to GHCR
|
||||
|
||||
```bash
|
||||
docker build -t ghcr.io/nmemmert/podcast-checklist:latest .
|
||||
docker push ghcr.io/nmemmert/podcast-checklist:latest
|
||||
```
|
||||
|
||||
### 2. On the Oracle Cloud VM
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/nmemmert/podcast-checklist:latest
|
||||
docker rm -f podcast-checklist 2>/dev/null || true
|
||||
docker run -d \
|
||||
--name podcast-checklist \
|
||||
--restart unless-stopped \
|
||||
-p 3001:3000 \
|
||||
-v podcast-checklist-data:/data \
|
||||
ghcr.io/nmemmert/podcast-checklist:latest
|
||||
```
|
||||
|
||||
> Uses port **3001** on the host so it doesn't clash with your existing site on 3000.
|
||||
> Progress is saved to a named Docker volume (`podcast-checklist-data`) so it survives container restarts and re-deploys.
|
||||
|
||||
### 3. Cloudflare DNS
|
||||
|
||||
Add a CNAME record in Cloudflare:
|
||||
- **Name:** `checklist` (or whatever subdomain you want)
|
||||
- **Target:** `versebyversewithnate.us`
|
||||
- **Proxy:** Enabled (orange cloud)
|
||||
|
||||
### 4. Cloudflare Tunnel or Nginx proxy
|
||||
|
||||
If you're using Nginx Proxy Manager or a Cloudflare Tunnel on the VM, add a rule to forward `checklist.versebyversewithnate.us` → `localhost:3001`.
|
||||
|
||||
---
|
||||
|
||||
## Update workflow
|
||||
|
||||
Same as your site:
|
||||
|
||||
```bash
|
||||
# Local
|
||||
docker build -t ghcr.io/nmemmert/podcast-checklist:latest .
|
||||
docker push ghcr.io/nmemmert/podcast-checklist:latest
|
||||
|
||||
# SSH into VM
|
||||
docker pull ghcr.io/nmemmert/podcast-checklist:latest
|
||||
docker rm -f podcast-checklist
|
||||
docker run -d \
|
||||
--name podcast-checklist \
|
||||
--restart unless-stopped \
|
||||
-p 3001:3000 \
|
||||
-v podcast-checklist-data:/data \
|
||||
ghcr.io/nmemmert/podcast-checklist:latest
|
||||
```
|
||||
|
||||
Progress data is in the named volume and is **not affected** by pulling a new image.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "podcast-checklist",
|
||||
"version": "1.0.0",
|
||||
"description": "Verse by Verse with Nate — Episode Production Checklist",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Episode Checklist — Verse by Verse with Nate</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,600&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--black: #0d0d0a;
|
||||
--card-bg: #111108;
|
||||
--border: #1e1c14;
|
||||
--border-mid: #2a2518;
|
||||
--gold: #c9a84c;
|
||||
--gold-light: #e0c070;
|
||||
--warm-white: #f0ead8;
|
||||
--warm-gray: #7a7060;
|
||||
--muted: #4a4438;
|
||||
--dim: #3a3020;
|
||||
--deep: #252018;
|
||||
}
|
||||
|
||||
body { background: var(--black); color: var(--warm-white); font-family: 'Cormorant Garamond', Georgia, serif; min-height: 100vh; }
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: var(--black); }
|
||||
::-webkit-scrollbar-thumb { background: var(--border-mid); }
|
||||
|
||||
.root { max-width: 720px; margin: 0 auto; padding: 48px 20px 100px; }
|
||||
|
||||
.masthead { text-align: center; margin-bottom: 40px; }
|
||||
.eyebrow { font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--warm-gray); margin-bottom: 10px; }
|
||||
.main-title { font-size: clamp(28px, 6vw, 44px); font-weight: 300; font-style: italic; color: var(--gold); line-height: 1.1; margin-bottom: 4px; }
|
||||
.subtitle { font-size: 13px; color: var(--muted); letter-spacing: 0.1em; }
|
||||
.gold-rule { width: 80px; height: 1px; background: linear-gradient(90deg, transparent, var(--gold), transparent); margin: 18px auto; }
|
||||
|
||||
.save-status { text-align: center; font-size: 11px; letter-spacing: 0.12em; color: var(--dim); margin-bottom: 24px; height: 16px; transition: color 0.3s; }
|
||||
.save-status.saving { color: var(--warm-gray); }
|
||||
.save-status.saved { color: var(--gold); }
|
||||
.save-status.error { color: #8b3030; }
|
||||
|
||||
.overall { background: var(--card-bg); border: 1px solid var(--border); border-radius: 2px; padding: 18px 22px; margin-bottom: 32px; }
|
||||
.overall-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
|
||||
.overall-label { font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--warm-gray); }
|
||||
.overall-pct { font-size: 28px; font-weight: 300; color: var(--gold); }
|
||||
.bar-wrap { height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; margin-bottom: 14px; }
|
||||
.bar-fill { height: 100%; background: linear-gradient(90deg, var(--gold), var(--gold-light)); border-radius: 2px; transition: width 0.4s ease; }
|
||||
.series-stats { display: flex; gap: 24px; flex-wrap: wrap; }
|
||||
.stat { font-size: 12px; color: var(--muted); }
|
||||
.stat b { color: var(--gold); font-weight: 500; }
|
||||
|
||||
.filters { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
|
||||
.filter-btn { font-family: 'Cormorant Garamond', serif; font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; background: none; border: 1px solid var(--border-mid); color: var(--muted); padding: 6px 14px; border-radius: 1px; cursor: pointer; transition: all 0.15s; }
|
||||
.filter-btn:hover { color: var(--gold); border-color: var(--dim); }
|
||||
.filter-btn.active { color: var(--gold); border-color: var(--gold); background: var(--card-bg); }
|
||||
|
||||
.section-label { font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--dim); margin: 28px 0 12px; padding-left: 2px; }
|
||||
|
||||
/* Card */
|
||||
.card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 2px; margin-bottom: 8px; overflow: hidden; transition: border-color 0.2s; }
|
||||
.card:hover { border-color: var(--border-mid); }
|
||||
.card.complete { border-left: 2px solid var(--gold); }
|
||||
|
||||
.card-header { display: flex; align-items: center; gap: 12px; padding: 14px 18px; cursor: pointer; user-select: none; }
|
||||
|
||||
.series-tag { font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; padding: 2px 7px; border-radius: 1px; flex-shrink: 0; }
|
||||
.tag-Titus { color: var(--gold); background: #1a1810; border: 1px solid var(--border-mid); }
|
||||
.tag-Colossians { color: var(--warm-gray); background: #131310; border: 1px solid var(--border); }
|
||||
|
||||
.ep-num { font-size: 13px; font-weight: 600; color: var(--muted); flex-shrink: 0; min-width: 36px; }
|
||||
.card.complete .ep-num { color: var(--gold); }
|
||||
|
||||
.ep-label { flex: 1; font-size: 15px; color: #c8c0a8; }
|
||||
.card.complete .ep-label { color: var(--muted); }
|
||||
|
||||
.pub-date-pill { font-size: 10px; color: var(--warm-gray); font-style: italic; flex-shrink: 0; letter-spacing: 0.03em; }
|
||||
|
||||
.task-count { font-size: 11px; color: var(--dim); flex-shrink: 0; }
|
||||
.card.complete .task-count { color: var(--gold); }
|
||||
|
||||
.mini-bar { width: 48px; height: 2px; background: var(--border); border-radius: 1px; overflow: hidden; flex-shrink: 0; }
|
||||
.mini-fill { height: 100%; background: linear-gradient(90deg, var(--gold), var(--gold-light)); transition: width 0.3s; }
|
||||
|
||||
.chevron { font-size: 10px; color: var(--border-mid); transition: transform 0.2s, color 0.15s; flex-shrink: 0; }
|
||||
.chevron.open { transform: rotate(180deg); color: var(--muted); }
|
||||
|
||||
.card-body { border-top: 1px solid #1a1810; padding: 0 18px 18px; }
|
||||
|
||||
/* Date published row */
|
||||
.date-row { display: flex; align-items: center; gap: 12px; padding: 14px 0 12px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
|
||||
.date-label { font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--warm-gray); flex-shrink: 0; }
|
||||
.date-input {
|
||||
background: transparent; border: none; border-bottom: 1px solid var(--border-mid);
|
||||
outline: none; font-family: 'Cormorant Garamond', serif; font-size: 14px;
|
||||
color: var(--gold-light); padding: 2px 6px 3px; cursor: pointer;
|
||||
color-scheme: dark;
|
||||
}
|
||||
.date-input:focus { border-bottom-color: var(--gold); }
|
||||
.date-input::-webkit-calendar-picker-indicator { filter: invert(0.6) sepia(1) saturate(2) hue-rotate(5deg); cursor: pointer; }
|
||||
|
||||
/* Phase headers */
|
||||
.phase-header { font-size: 10px; letter-spacing: 0.25em; text-transform: uppercase; color: var(--warm-gray); padding: 14px 0 6px; border-bottom: 1px solid var(--border); margin-bottom: 2px; display: flex; align-items: center; gap: 8px; }
|
||||
.phase-header::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
||||
.phase-pre { color: var(--warm-gray); }
|
||||
.phase-post { color: var(--gold); }
|
||||
|
||||
.task-list { list-style: none; }
|
||||
.task-item { display: flex; align-items: flex-start; gap: 12px; padding: 9px 0; border-bottom: 1px solid #141210; cursor: pointer; }
|
||||
.task-item:last-child { border-bottom: none; }
|
||||
|
||||
.check-box { width: 16px; height: 16px; border: 1px solid var(--border-mid); border-radius: 1px; flex-shrink: 0; margin-top: 3px; display: flex; align-items: center; justify-content: center; transition: background 0.15s, border-color 0.15s; }
|
||||
.task-item.done .check-box { background: var(--gold); border-color: var(--gold); }
|
||||
.check-svg { display: none; }
|
||||
.task-item.done .check-svg { display: block; }
|
||||
|
||||
.step-num { font-size: 9px; letter-spacing: 0.15em; color: var(--border-mid); margin-top: 4px; flex-shrink: 0; width: 22px; }
|
||||
|
||||
.task-body { flex: 1; }
|
||||
.task-name { font-size: 14px; font-weight: 500; color: #d8d0b8; line-height: 1.3; transition: color 0.15s; }
|
||||
.task-item.done .task-name { color: var(--dim); text-decoration: line-through; text-decoration-color: var(--border-mid); }
|
||||
.task-hint { font-size: 12px; color: var(--muted); font-style: italic; margin-top: 1px; }
|
||||
.task-item.done .task-hint { color: var(--deep); }
|
||||
.task-item:hover .task-name { color: var(--warm-white); }
|
||||
.task-item.done:hover .task-name { color: var(--dim); }
|
||||
|
||||
.card-footer { display: flex; justify-content: flex-end; padding-top: 10px; margin-top: 4px; border-top: 1px solid #141210; }
|
||||
.btn-reset { font-family: 'Cormorant Garamond', serif; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; background: none; border: 1px solid var(--border); color: var(--dim); padding: 4px 10px; border-radius: 1px; cursor: pointer; transition: all 0.15s; }
|
||||
.btn-reset:hover { color: var(--warm-gray); border-color: var(--border-mid); }
|
||||
.complete-badge { font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--gold); padding: 2px 8px; border: 1px solid var(--border-mid); border-radius: 1px; }
|
||||
|
||||
.loading { text-align: center; color: var(--muted); font-style: italic; padding: 40px; font-size: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="root">
|
||||
|
||||
<div class="masthead">
|
||||
<div class="eyebrow">Verse by Verse with Nate</div>
|
||||
<div class="main-title">Production Checklist</div>
|
||||
<div class="subtitle">Titus · Colossians</div>
|
||||
<div class="gold-rule"></div>
|
||||
</div>
|
||||
|
||||
<div class="save-status" id="saveStatus"></div>
|
||||
|
||||
<div class="overall">
|
||||
<div class="overall-row">
|
||||
<div class="overall-label">Overall Progress</div>
|
||||
<div class="overall-pct" id="overallPct">0%</div>
|
||||
</div>
|
||||
<div class="bar-wrap"><div class="bar-fill" id="overallBar" style="width:0%"></div></div>
|
||||
<div class="series-stats">
|
||||
<div class="stat">Titus — <b id="titusDone">0</b> / 5 episodes done</div>
|
||||
<div class="stat">Colossians — <b id="colossDone">0</b> / 27 episodes done</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<button class="filter-btn active" onclick="setFilter('All')">All</button>
|
||||
<button class="filter-btn" onclick="setFilter('Titus')">Titus</button>
|
||||
<button class="filter-btn" onclick="setFilter('Colossians')">Colossians</button>
|
||||
</div>
|
||||
|
||||
<div id="app"><div class="loading">Loading…</div></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Task definitions ────────────────────────────────────────────────────────
|
||||
const PRE_TASKS = [
|
||||
{ id: "verify_script", step: "01", label: "Verify Script", desc: "Review for accuracy, theology, and flow" },
|
||||
{ id: "read_script", step: "02", label: "Read Script", desc: "Read aloud, mark pacing & emphasis" },
|
||||
{ id: "record", step: "03", label: "Record", desc: "Capture final audio take" },
|
||||
{ id: "edit", step: "04", label: "Edit", desc: "Trim, cut, clean up in Ferrite / GarageBand" },
|
||||
{ id: "mix", step: "05", label: "Mix", desc: "Levels, EQ, music beds, final master" },
|
||||
{ id: "video_script", step: "06", label: "Run Video Conversion Script", desc: "Generate video file from audio" },
|
||||
{ id: "post_spotify", step: "07", label: "Post on Spotify", desc: "Upload with show notes & discussion questions" },
|
||||
];
|
||||
|
||||
const POST_TASKS = [
|
||||
{ id: "update_website", step: "08", label: "Update Website", desc: "Add episode highlights to versebyversewithnate.us" },
|
||||
{ id: "send_email", step: "09", label: "Send Email", desc: "Send newsletter to subscribers via Resend" },
|
||||
];
|
||||
|
||||
const ALL_TASK_IDS = [...PRE_TASKS, ...POST_TASKS].map(t => t.id);
|
||||
|
||||
function makeEpisode(series, number) {
|
||||
const tasks = {};
|
||||
ALL_TASK_IDS.forEach(id => tasks[id] = false);
|
||||
return { id: `${series}-${number}`, series, number, tasks, datePublished: "", expanded: false };
|
||||
}
|
||||
|
||||
function buildEpisodes() {
|
||||
const titus = [11,12,13,14,15].map(n => makeEpisode("Titus", n));
|
||||
const colossians = Array.from({ length: 27 }, (_, i) => makeEpisode("Colossians", i + 1));
|
||||
return [...titus, ...colossians];
|
||||
}
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────────
|
||||
let episodes = [];
|
||||
let activeFilter = "All";
|
||||
let saveTimer = null;
|
||||
|
||||
// ── Server I/O ──────────────────────────────────────────────────────────────
|
||||
async function loadFromServer() {
|
||||
try {
|
||||
const res = await fetch("/api/progress");
|
||||
const saved = await res.json();
|
||||
const base = buildEpisodes();
|
||||
episodes = base.map(ep => {
|
||||
const s = saved[ep.id];
|
||||
if (!s) return ep;
|
||||
return {
|
||||
...ep,
|
||||
expanded: s.expanded ?? false,
|
||||
datePublished: s.datePublished ?? "",
|
||||
tasks: Object.fromEntries(ALL_TASK_IDS.map(id => [id, s.tasks?.[id] ?? false]))
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
episodes = buildEpisodes();
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function scheduleSave() {
|
||||
clearTimeout(saveTimer);
|
||||
setStatus("saving", "Saving…");
|
||||
saveTimer = setTimeout(saveToServer, 800);
|
||||
}
|
||||
|
||||
async function saveToServer() {
|
||||
const payload = {};
|
||||
episodes.forEach(ep => {
|
||||
payload[ep.id] = { expanded: ep.expanded, datePublished: ep.datePublished, tasks: ep.tasks };
|
||||
});
|
||||
try {
|
||||
await fetch("/api/progress", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
setStatus("saved", "✦ Saved");
|
||||
setTimeout(() => setStatus("", ""), 2000);
|
||||
} catch {
|
||||
setStatus("error", "Save failed — check connection");
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(cls, msg) {
|
||||
const el = document.getElementById("saveStatus");
|
||||
el.className = "save-status" + (cls ? " " + cls : "");
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
// ── Mutations ────────────────────────────────────────────────────────────────
|
||||
function toggleTask(epId, taskId) {
|
||||
const ep = episodes.find(e => e.id === epId);
|
||||
ep.tasks[taskId] = !ep.tasks[taskId];
|
||||
scheduleSave(); render();
|
||||
}
|
||||
|
||||
function toggleExpand(epId) {
|
||||
const ep = episodes.find(e => e.id === epId);
|
||||
ep.expanded = !ep.expanded;
|
||||
scheduleSave(); render();
|
||||
}
|
||||
|
||||
function setDate(epId, val) {
|
||||
const ep = episodes.find(e => e.id === epId);
|
||||
ep.datePublished = val;
|
||||
scheduleSave(); render();
|
||||
}
|
||||
|
||||
function resetEpisode(epId) {
|
||||
const ep = episodes.find(e => e.id === epId);
|
||||
ALL_TASK_IDS.forEach(id => ep.tasks[id] = false);
|
||||
ep.datePublished = "";
|
||||
scheduleSave(); render();
|
||||
}
|
||||
|
||||
function setFilter(f) {
|
||||
activeFilter = f;
|
||||
document.querySelectorAll(".filter-btn").forEach(btn =>
|
||||
btn.classList.toggle("active", btn.textContent === f)
|
||||
);
|
||||
render();
|
||||
}
|
||||
|
||||
// ── Render ───────────────────────────────────────────────────────────────────
|
||||
function render() {
|
||||
updateStats();
|
||||
const app = document.getElementById("app");
|
||||
const filtered = activeFilter === "All" ? episodes : episodes.filter(e => e.series === activeFilter);
|
||||
const titus = filtered.filter(e => e.series === "Titus");
|
||||
const colossians = filtered.filter(e => e.series === "Colossians");
|
||||
|
||||
let html = "";
|
||||
if (titus.length) {
|
||||
html += `<div class="section-label">Titus — Episodes 11–15</div>`;
|
||||
titus.forEach(ep => html += renderCard(ep));
|
||||
}
|
||||
if (colossians.length) {
|
||||
html += `<div class="section-label">Colossians — Episodes 1–27</div>`;
|
||||
colossians.forEach(ep => html += renderCard(ep));
|
||||
}
|
||||
app.innerHTML = html;
|
||||
}
|
||||
|
||||
function formatDate(val) {
|
||||
if (!val) return "";
|
||||
const [y, m, d] = val.split("-");
|
||||
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||
return `${months[parseInt(m)-1]} ${parseInt(d)}, ${y}`;
|
||||
}
|
||||
|
||||
function renderCard(ep) {
|
||||
const allDone = ALL_TASK_IDS.every(id => ep.tasks[id]);
|
||||
const doneCount = ALL_TASK_IDS.filter(id => ep.tasks[id]).length;
|
||||
const total = ALL_TASK_IDS.length;
|
||||
const pct = Math.round((doneCount / total) * 100);
|
||||
const dateDisplay = ep.datePublished ? formatDate(ep.datePublished) : "";
|
||||
|
||||
let body = "";
|
||||
if (ep.expanded) {
|
||||
// Pre-publish tasks
|
||||
const preTasks = PRE_TASKS.map(t => renderTask(ep.id, t, ep.tasks[t.id])).join("");
|
||||
// Post-publish tasks
|
||||
const postTasks = POST_TASKS.map(t => renderTask(ep.id, t, ep.tasks[t.id])).join("");
|
||||
|
||||
const footer = allDone
|
||||
? `<span class="complete-badge">✦ Complete</span>`
|
||||
: `<button class="btn-reset" onclick="event.stopPropagation();resetEpisode('${ep.id}')">Reset</button>`;
|
||||
|
||||
body = `
|
||||
<div class="card-body">
|
||||
<div class="date-row">
|
||||
<span class="date-label">Date Published</span>
|
||||
<input type="date" class="date-input" value="${ep.datePublished}"
|
||||
onchange="setDate('${ep.id}', this.value)"
|
||||
onclick="event.stopPropagation()" />
|
||||
${ep.datePublished ? `<span style="font-size:12px;color:var(--warm-gray);font-style:italic;">${formatDate(ep.datePublished)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="phase-header phase-pre">Pre-Publish</div>
|
||||
<ul class="task-list">${preTasks}</ul>
|
||||
|
||||
<div class="phase-header phase-post" style="margin-top:10px;">Post-Publish</div>
|
||||
<ul class="task-list">${postTasks}</ul>
|
||||
|
||||
<div class="card-footer">${footer}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="card${allDone ? " complete" : ""}">
|
||||
<div class="card-header" onclick="toggleExpand('${ep.id}')">
|
||||
<span class="series-tag tag-${ep.series}">${ep.series}</span>
|
||||
<span class="ep-num">Ep ${ep.number}</span>
|
||||
<span class="ep-label">${ep.series} — Episode ${ep.number}</span>
|
||||
${dateDisplay ? `<span class="pub-date-pill">${dateDisplay}</span>` : ""}
|
||||
<span class="task-count">${doneCount}/${total}</span>
|
||||
<div class="mini-bar"><div class="mini-fill" style="width:${pct}%"></div></div>
|
||||
<span class="chevron${ep.expanded ? " open" : ""}">▼</span>
|
||||
</div>
|
||||
${body}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderTask(epId, t, done) {
|
||||
return `
|
||||
<li class="task-item${done ? " done" : ""}" onclick="toggleTask('${epId}','${t.id}')">
|
||||
<div class="check-box">
|
||||
<svg class="check-svg" width="10" height="8" viewBox="0 0 10 8" fill="none">
|
||||
<path d="M1 4L3.5 6.5L9 1" stroke="#0d0d0a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-num">${t.step}</span>
|
||||
<div class="task-body">
|
||||
<div class="task-name">${t.label}</div>
|
||||
<div class="task-hint">${t.desc}</div>
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const totalDone = episodes.reduce((a, e) => a + ALL_TASK_IDS.filter(id => e.tasks[id]).length, 0);
|
||||
const totalTasks = episodes.length * ALL_TASK_IDS.length;
|
||||
const pct = Math.round((totalDone / totalTasks) * 100);
|
||||
document.getElementById("overallPct").textContent = pct + "%";
|
||||
document.getElementById("overallBar").style.width = pct + "%";
|
||||
document.getElementById("titusDone").textContent =
|
||||
episodes.filter(e => e.series === "Titus" && ALL_TASK_IDS.every(id => e.tasks[id])).length;
|
||||
document.getElementById("colossDone").textContent =
|
||||
episodes.filter(e => e.series === "Colossians" && ALL_TASK_IDS.every(id => e.tasks[id])).length;
|
||||
}
|
||||
|
||||
loadFromServer();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA_FILE = process.env.DATA_FILE || "/data/progress.json";
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
// Ensure data directory and file exist
|
||||
function ensureDataFile() {
|
||||
const dir = path.dirname(DATA_FILE);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
if (!fs.existsSync(DATA_FILE)) fs.writeFileSync(DATA_FILE, JSON.stringify({}));
|
||||
}
|
||||
|
||||
// GET progress
|
||||
app.get("/api/progress", (req, res) => {
|
||||
try {
|
||||
ensureDataFile();
|
||||
const data = JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error("Read error:", err);
|
||||
res.status(500).json({ error: "Failed to read progress" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST progress (full state)
|
||||
app.post("/api/progress", (req, res) => {
|
||||
try {
|
||||
ensureDataFile();
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(req.body, null, 2));
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error("Write error:", err);
|
||||
res.status(500).json({ error: "Failed to save progress" });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`Podcast checklist running on port ${PORT}`));
|
||||
Reference in New Issue
Block a user