@@ -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 : 100 vh ; }
:: -webkit-scrollbar { width : 4 px ; }
:: -webkit-scrollbar-track { background : var ( - - black ) ; }
:: -webkit-scrollbar-thumb { background : var ( - - border - mid ) ; }
. root { max-width : 720 px ; margin : 0 auto ; padding : 48 px 20 px 100 px ; }
. masthead { text-align : center ; margin-bottom : 40 px ; }
. eyebrow { font-size : 10 px ; letter-spacing : 0.3 em ; text-transform : uppercase ; color : var ( - - warm - gray ) ; margin-bottom : 10 px ; }
. main-title { font-size : clamp ( 28 px , 6 vw , 44 px ) ; font-weight : 300 ; font-style : italic ; color : var ( - - gold ) ; line-height : 1.1 ; margin-bottom : 4 px ; }
. subtitle { font-size : 13 px ; color : var ( - - muted ) ; letter-spacing : 0.1 em ; }
. gold-rule { width : 80 px ; height : 1 px ; background : linear-gradient ( 90 deg , transparent , var ( - - gold ) , transparent ) ; margin : 18 px auto ; }
. save-status { text-align : center ; font-size : 11 px ; letter-spacing : 0.12 em ; color : var ( - - dim ) ; margin-bottom : 24 px ; height : 16 px ; transition : color 0.3 s ; }
. save-status . saving { color : var ( - - warm - gray ) ; }
. save-status . saved { color : var ( - - gold ) ; }
. save-status . error { color : #8b3030 ; }
. overall { background : var ( - - card - bg ) ; border : 1 px solid var ( - - border ) ; border-radius : 2 px ; padding : 18 px 22 px ; margin-bottom : 32 px ; }
. overall-row { display : flex ; justify-content : space-between ; align-items : baseline ; margin-bottom : 10 px ; }
. overall-label { font-size : 11 px ; letter-spacing : 0.2 em ; text-transform : uppercase ; color : var ( - - warm - gray ) ; }
. overall-pct { font-size : 28 px ; font-weight : 300 ; color : var ( - - gold ) ; }
. bar-wrap { height : 3 px ; background : var ( - - border ) ; border-radius : 2 px ; overflow : hidden ; margin-bottom : 14 px ; }
. bar-fill { height : 100 % ; background : linear-gradient ( 90 deg , var ( - - gold ) , var ( - - gold - light ) ) ; border-radius : 2 px ; transition : width 0.4 s ease ; }
. series-stats { display : flex ; gap : 24 px ; flex-wrap : wrap ; }
. stat { font-size : 12 px ; color : var ( - - muted ) ; }
. stat b { color : var ( - - gold ) ; font-weight : 500 ; }
. filters { display : flex ; gap : 8 px ; margin-bottom : 24 px ; flex-wrap : wrap ; }
. filter-btn { font-family : 'Cormorant Garamond' , serif ; font-size : 11 px ; letter-spacing : 0.2 em ; text-transform : uppercase ; background : none ; border : 1 px solid var ( - - border - mid ) ; color : var ( - - muted ) ; padding : 6 px 14 px ; border-radius : 1 px ; cursor : pointer ; transition : all 0.15 s ; }
. 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 : 10 px ; letter-spacing : 0.3 em ; text-transform : uppercase ; color : var ( - - dim ) ; margin : 28 px 0 12 px ; padding-left : 2 px ; }
/* Card */
. card { background : var ( - - card - bg ) ; border : 1 px solid var ( - - border ) ; border-radius : 2 px ; margin-bottom : 8 px ; overflow : hidden ; transition : border-color 0.2 s ; }
. card : hover { border-color : var ( - - border - mid ) ; }
. card . complete { border-left : 2 px solid var ( - - gold ) ; }
. card-header { display : flex ; align-items : center ; gap : 12 px ; padding : 14 px 18 px ; cursor : pointer ; user-select : none ; }
. series-tag { font-size : 9 px ; letter-spacing : 0.2 em ; text-transform : uppercase ; padding : 2 px 7 px ; border-radius : 1 px ; flex-shrink : 0 ; }
. tag-Titus { color : var ( - - gold ) ; background : #1a1810 ; border : 1 px solid var ( - - border - mid ) ; }
. tag-Colossians { color : var ( - - warm - gray ) ; background : #131310 ; border : 1 px solid var ( - - border ) ; }
. ep-num { font-size : 13 px ; font-weight : 600 ; color : var ( - - muted ) ; flex-shrink : 0 ; min-width : 36 px ; }
. card . complete . ep-num { color : var ( - - gold ) ; }
. ep-label { flex : 1 ; font-size : 15 px ; color : #c8c0a8 ; }
. card . complete . ep-label { color : var ( - - muted ) ; }
. pub-date-pill { font-size : 10 px ; color : var ( - - warm - gray ) ; font-style : italic ; flex-shrink : 0 ; letter-spacing : 0.03 em ; }
. task-count { font-size : 11 px ; color : var ( - - dim ) ; flex-shrink : 0 ; }
. card . complete . task-count { color : var ( - - gold ) ; }
. mini-bar { width : 48 px ; height : 2 px ; background : var ( - - border ) ; border-radius : 1 px ; overflow : hidden ; flex-shrink : 0 ; }
. mini-fill { height : 100 % ; background : linear-gradient ( 90 deg , var ( - - gold ) , var ( - - gold - light ) ) ; transition : width 0.3 s ; }
. chevron { font-size : 10 px ; color : var ( - - border - mid ) ; transition : transform 0.2 s , color 0.15 s ; flex-shrink : 0 ; }
. chevron . open { transform : rotate ( 180 deg ) ; color : var ( - - muted ) ; }
. card-body { border-top : 1 px solid #1a1810 ; padding : 0 18 px 18 px ; }
/* Date published row */
. date-row { display : flex ; align-items : center ; gap : 12 px ; padding : 14 px 0 12 px ; border-bottom : 1 px solid var ( - - border ) ; margin-bottom : 4 px ; }
. date-label { font-size : 10 px ; letter-spacing : 0.2 em ; text-transform : uppercase ; color : var ( - - warm - gray ) ; flex-shrink : 0 ; }
. date-input {
background : transparent ; border : none ; border-bottom : 1 px solid var ( - - border - mid ) ;
outline : none ; font-family : 'Cormorant Garamond' , serif ; font-size : 14 px ;
color : var ( - - gold - light ) ; padding : 2 px 6 px 3 px ; 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 ( 5 deg ) ; cursor : pointer ; }
/* Phase headers */
. phase-header { font-size : 10 px ; letter-spacing : 0.25 em ; text-transform : uppercase ; color : var ( - - warm - gray ) ; padding : 14 px 0 6 px ; border-bottom : 1 px solid var ( - - border ) ; margin-bottom : 2 px ; display : flex ; align-items : center ; gap : 8 px ; }
. phase-header :: after { content : '' ; flex : 1 ; height : 1 px ; 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 : 12 px ; padding : 9 px 0 ; border-bottom : 1 px solid #141210 ; cursor : pointer ; }
. task-item : last-child { border-bottom : none ; }
. check-box { width : 16 px ; height : 16 px ; border : 1 px solid var ( - - border - mid ) ; border-radius : 1 px ; flex-shrink : 0 ; margin-top : 3 px ; display : flex ; align-items : center ; justify-content : center ; transition : background 0.15 s , border-color 0.15 s ; }
. 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 : 9 px ; letter-spacing : 0.15 em ; color : var ( - - border - mid ) ; margin-top : 4 px ; flex-shrink : 0 ; width : 22 px ; }
. task-body { flex : 1 ; }
. task-name { font-size : 14 px ; font-weight : 500 ; color : #d8d0b8 ; line-height : 1.3 ; transition : color 0.15 s ; }
. task-item . done . task-name { color : var ( - - dim ) ; text-decoration : line-through ; text-decoration-color : var ( - - border - mid ) ; }
. task-hint { font-size : 12 px ; color : var ( - - muted ) ; font-style : italic ; margin-top : 1 px ; }
. 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 : 10 px ; margin-top : 4 px ; border-top : 1 px solid #141210 ; }
. btn-reset { font-family : 'Cormorant Garamond' , serif ; font-size : 10 px ; letter-spacing : 0.18 em ; text-transform : uppercase ; background : none ; border : 1 px solid var ( - - border ) ; color : var ( - - dim ) ; padding : 4 px 10 px ; border-radius : 1 px ; cursor : pointer ; transition : all 0.15 s ; }
. btn-reset : hover { color : var ( - - warm - gray ) ; border-color : var ( - - border - mid ) ; }
. complete-badge { font-size : 9 px ; letter-spacing : 0.2 em ; text-transform : uppercase ; color : var ( - - gold ) ; padding : 2 px 8 px ; border : 1 px solid var ( - - border - mid ) ; border-radius : 1 px ; }
. loading { text-align : center ; color : var ( - - muted ) ; font-style : italic ; padding : 40 px ; font-size : 15 px ; }
< / 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 >