From affd903f40ba5c03e9ec847cd774ad4c98edb3ab Mon Sep 17 00:00:00 2001 From: nmemmert Date: Tue, 12 May 2026 12:47:13 -0400 Subject: [PATCH] Make homepage study card admin-editable with NEW tag controls --- data/admin-content-draft.json | 820 +++++++++++++++++++++++++++++++++- data/admin-content.json | 812 ++++++++++++++++++++++++++++++++- data/questions.json | 2 +- data/study-notes.json | 8 + data/study-users.json | 13 + server.js | 340 +++++++++++++- server/helpers.js | 49 ++ src/AdminPage.tsx | 323 ++++++++++++- src/App.css | 485 ++++++++++++++++++++ src/App.tsx | 28 +- src/colossiansStudy.tsx | 697 +++++++++++++++++++++++++++++ src/colossiansStudyData.ts | 81 ++++ src/content.ts | 61 +++ 13 files changed, 3701 insertions(+), 18 deletions(-) create mode 100644 data/study-notes.json create mode 100644 data/study-users.json create mode 100644 src/colossiansStudy.tsx create mode 100644 src/colossiansStudyData.ts diff --git a/data/admin-content-draft.json b/data/admin-content-draft.json index 2930adc..8b3bef0 100644 --- a/data/admin-content-draft.json +++ b/data/admin-content-draft.json @@ -48,6 +48,824 @@ "studyGuideDownloadUrl": "", "studyGuideUrl": "https://a.co/d/01sG2tOJ", "studyGuideAmazonButtonLabel": "Get it on Amazon", + "studies": [ + { + "id": "study-colossians", + "slug": "colossians", + "title": "Colossians: Rooted in Christ", + "description": "Walk through Colossians in guided lessons with commentary, Greek notes, and discussion prompts.", + "status": "active", + "sections": [ + { + "id": "1-1-2", + "chapter": 1, + "reference": "1:1-2", + "title": "Paul’s Greeting", + "audioEmbedUrl": "", + "passageText": "v. 1 Paul, an apostle of Christ Jesus by the will of God, and Timothy our brother,\n\nv. 2 To the saints in Colossae, the faithful brothers in Christ: Grace and peace to you from God our Father.", + "summary": "Paul opens with apostolic authority and warm fellowship, greeting the saints at Colossae with the twin gifts that frame every believer's life: grace and peace.", + "commentary": "Paul opens with his credentials: an apostle of Christ Jesus. But the manner of the claim matters - he immediately attributes this not to his own merit but to the will of God. This is the same humility that runs beneath the letter's entire argument: everything comes from God, not from human achievement. Timothy is included not as a co-author but as a fellow laborer, identified simply as 'our brother' - the relational warmth is immediate. The recipients are addressed as 'saints' - the Greek hagios means holy ones, set apart by God - and as 'faithful brothers in Christ.' These are not terms of flattery but of theological definition: these are people who belong to God. Paul's greeting, 'grace and peace,' is more than convention. Grace (charis) is God's unmerited favor; peace (eirene) is the wholeness and well-being that flows from it. Every believer's life is framed by these two realities.", + "studyQuestions": [ + "Why does Paul begin with grace and peace?", + "What tone does this create for the rest of the study?" + ], + "greekNotes": [ + "Apostle (apostolos): one commissioned directly by Jesus Christ, invested with authority to speak on His behalf.", + "Will (thelema): desire or purpose; Paul was not self-appointed but called by God’s initiative.", + "Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.", + "Saints (hagios): God’s holy people set apart as belonging to Him, not holy by their own merit.", + "Grace (charis): God's unmerited favor that initiates and sustains the believer's life.", + "Peace (eirene): wholeness and well-being that flows from reconciliation with God." + ], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "1-3-8", + "chapter": 1, + "reference": "1:3-8", + "title": "Thanksgiving and Report", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul thanks God for faith, hope, and love, and reports that the gospel is bearing fruit.", + "commentary": "The gospel is shown to be active and alive. Paul points to evidence of real spiritual growth, not merely religious activity.", + "studyQuestions": [ + "What signs of gospel growth does Paul mention?", + "How do faith, hope, and love work together here?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "1-9-14", + "chapter": 1, + "reference": "1:9-14", + "title": "Paul’s Prayer", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul prays for spiritual wisdom, endurance, and a life worthy of the Lord.", + "commentary": "This prayer shows that knowledge and fruitfulness belong together. Paul wants the believers to know God’s will and walk in a way that reflects it.", + "studyQuestions": [ + "What does Paul ask God to produce?", + "How does this prayer shape the goals of the study?" + ], + "greekNotes": [] + }, + { + "id": "1-15-20", + "chapter": 1, + "reference": "1:15-20", + "title": "The Supremacy of Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Christ is supreme over creation, the church, and reconciliation.", + "commentary": "This is the center of Colossians. Everything else in the letter depends on who Christ is and what He has done.", + "studyQuestions": [ + "What stands out most about Christ’s supremacy?", + "Why is this passage central to the study?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "1-21-23", + "chapter": 1, + "reference": "1:21-23", + "title": "Reconciliation Applied", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains how Christ’s work changes alienation into steadfast faith.", + "commentary": "The gospel moves from doctrine to real life. Paul shows what reconciliation looks like for people who have been brought near to God.", + "studyQuestions": [ + "How does Paul describe the believer’s former condition?", + "What does continuing in the faith look like?" + ], + "greekNotes": [], + "releasedAt": "2026-05-18T00:00:00Z" + }, + { + "id": "1-24-29", + "chapter": 1, + "reference": "1:24-29", + "title": "Paul’s Ministry and the Revealed Mystery", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.", + "commentary": "Paul’s ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.", + "studyQuestions": [ + "What is the goal of Paul’s ministry?", + "Why is maturity such an important theme here?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-1-5", + "chapter": 2, + "reference": "2:1-5", + "title": "Paul’s Pastoral Concern", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reveals his struggle for the believers and his desire for encouragement and unity.", + "commentary": "This passage shows the heart behind the letter. Paul is deeply invested in the spiritual stability of the church.", + "studyQuestions": [ + "What does Paul want the believers to experience?", + "Why does he emphasize encouragement and unity?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-6-8", + "chapter": 2, + "reference": "2:6-8", + "title": "Walk in Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to continue in Christ and resist deceptive teaching.", + "commentary": "The Christian life begins in Christ and must continue in Christ. Paul warns that subtle deception can slowly pull believers off course.", + "studyQuestions": [ + "What does it mean to continue in Christ?", + "What kinds of deception should believers watch for?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-9-10", + "chapter": 2, + "reference": "2:9-10", + "title": "Supremacy and Sufficiency", + "audioEmbedUrl": "", + "passageText": "", + "summary": "The fullness of deity dwells in Christ, and believers are made complete in Him.", + "commentary": "This section corrects the idea that anything must be added to Christ. He is fully sufficient for the believer’s life and growth.", + "studyQuestions": [ + "Why is fullness in Christ such an important correction?", + "How does this guard against spiritual insecurity?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-11-15", + "chapter": 2, + "reference": "2:11-15", + "title": "What Christ Has Done", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains the believer’s union with Christ in His death, resurrection, and victory.", + "commentary": "The work of Christ is presented as decisive and complete. This becomes the foundation for all the commands that follow.", + "studyQuestions": [ + "What changes because of Christ’s work here?", + "How does victory over the powers encourage believers today?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-16-17", + "chapter": 2, + "reference": "2:16-17", + "title": "Freedom from Legalistic Judgment", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against letting others judge believers over shadows when Christ is the substance.", + "commentary": "Outward practices can never replace Christ. Paul reminds the church that the real thing has come in Jesus.", + "studyQuestions": [ + "What do shadows and substance mean here?", + "Where do believers still feel pressure from external judgment?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-18-19", + "chapter": 2, + "reference": "2:18-19", + "title": "Warning Against False Mysticism", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against prideful spirituality that disconnects believers from Christ the head.", + "commentary": "Anything that pulls attention away from Christ and His body is spiritually dangerous. True growth stays connected to the head.", + "studyQuestions": [ + "How does Paul describe false spirituality?", + "Why is connection to Christ essential?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-20-23", + "chapter": 2, + "reference": "2:20-23", + "title": "The Emptiness of Human Regulations", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Human rules cannot produce true transformation.", + "commentary": "External restrictions may look wise, but they cannot change the heart. Paul points the church back to union with Christ.", + "studyQuestions": [ + "Why do human regulations fail to transform the heart?", + "What does real spiritual change require?" + ], + "greekNotes": [], + "releasedAt": "2026-06-01T00:00:00Z" + }, + { + "id": "3-1-4", + "chapter": 3, + "reference": "3:1-4", + "title": "Seek the Things Above", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to set their hearts and minds on Christ.", + "commentary": "Identity in Christ leads to a new direction for the mind and heart. The believer’s life is hidden with Christ, so priorities shift upward.", + "studyQuestions": [ + "What does it mean to seek the things above?", + "How does hidden life in Christ shape daily priorities?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-5-9", + "chapter": 3, + "reference": "3:5-9", + "title": "Put to Death the Old Life", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to put away the practices of the old life.", + "commentary": "Christian growth includes real moral change. Paul’s language is strong because the old life cannot be carried into the new one.", + "studyQuestions": [ + "Which old patterns does Paul name?", + "Why is decisive change necessary?" + ], + "greekNotes": [], + "releasedAt": "2026-06-01T00:00:00Z" + }, + { + "id": "3-10-11", + "chapter": 3, + "reference": "3:10-11", + "title": "Put On the New Self", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Believers are renewed in Christ and formed into one new people.", + "commentary": "Renewal in Christ changes both personal identity and community life. The new self is not merely private; it is shared by the body of Christ.", + "studyQuestions": [ + "What does renewal in Christ produce?", + "How does this reshape how believers see one another?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-12-14", + "chapter": 3, + "reference": "3:12-14", + "title": "Clothe Yourselves with Christlike Virtues", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul lists the virtues believers should wear as Christ’s character becomes visible in them.", + "commentary": "The image of clothing makes virtue practical and visible. Paul wants Christ’s character to define the way believers live together.", + "studyQuestions": [ + "Which virtues are most needed in your context?", + "How does love bind these virtues together?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-15-17", + "chapter": 3, + "reference": "3:15-17", + "title": "The Rule of Christ’s Peace and Word", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls the church to let Christ’s peace rule and His word dwell richly among them.", + "commentary": "Peace, gratitude, and the word of Christ shape a healthy Christian community. These verses connect worship, teaching, and daily life.", + "studyQuestions": [ + "What does it look like for Christ’s peace to rule?", + "How can the word of Christ dwell richly in a church family?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-18-21", + "chapter": 3, + "reference": "3:18-21", + "title": "Household Code — Relationships", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul applies the new life in Christ to family relationships.", + "commentary": "The gospel reaches into ordinary relationships. Christ changes how homes are ordered and how people treat one another.", + "studyQuestions": [ + "How do these instructions reflect Christ’s lordship?", + "Which relationship is most challenging to apply today?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-22-25", + "chapter": 3, + "reference": "3:22-25", + "title": "Household Code — Work as unto the Lord", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul instructs believers to serve faithfully and wholeheartedly.", + "commentary": "Work becomes worship when it is done for Christ. Paul teaches integrity, diligence, and accountability before a heavenly Master.", + "studyQuestions": [ + "How does serving the Lord reshape everyday work?", + "What does Paul say about fairness and accountability?" + ], + "greekNotes": [] + }, + { + "id": "4-1", + "chapter": 4, + "reference": "4:1", + "title": "Masters — Fairness Before a Heavenly Master", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reminds masters to act justly because they too answer to the Lord.", + "commentary": "The gospel confronts authority as well as submission. All authority is accountable to Christ.", + "studyQuestions": [ + "Why does Paul remind masters of their heavenly Master?", + "What does fairness look like in this context?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-2-6", + "chapter": 4, + "reference": "4:2-6", + "title": "Devoted to Prayer and Wise Witness", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul closes with a call to steadfast prayer and wise witness toward outsiders.", + "commentary": "Prayer and witness belong together. Paul shows that spiritual alertness and everyday speech are both part of faithful ministry.", + "studyQuestions": [ + "How are prayer and witness connected here?", + "What does gracious speech look like in real life?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-7-9", + "chapter": 4, + "reference": "4:7-9", + "title": "Tychicus and Onesimus", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul sends trusted messengers to encourage the church and bring news from the ministry field.", + "commentary": "These greetings reveal the relational side of ministry. The letter is carried by real people who serve the church with loyalty and care.", + "studyQuestions": [ + "Why do these messengers matter to the letter?", + "What does this tell us about trusted coworkers?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-10-14", + "chapter": 4, + "reference": "4:10-14", + "title": "Greetings from Paul’s Co-workers", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul names the coworkers who are with him and commends their ministry support.", + "commentary": "The closing greetings show the breadth of the gospel network. Ministry is shared, supported, and strengthened by faithful co-workers.", + "studyQuestions": [ + "What stands out about Paul’s coworkers?", + "How does shared ministry strengthen the church?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-15-18", + "chapter": 4, + "reference": "4:15-18", + "title": "Final Instructions and Closing", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul offers final greetings, instructions, and a closing reminder to complete the ministry entrusted to them.", + "commentary": "The letter ends with community, responsibility, and grace. Paul’s final words keep the church focused on the mission it has received.", + "studyQuestions": [ + "What final responsibility does Paul place on the church?", + "How does the closing reinforce the letter’s themes?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + } + ], + "homepageEyebrow": "", + "showOnHomepage": false, + "showNewTag": false, + "newTagLabel": "NEW", + "numberOfChapters": 1 + }, + { + "id": "study-titus", + "slug": "titus", + "title": "Titus: Sound Doctrine", + "description": "A full online class version of Titus with lesson-by-lesson notes and discussion.", + "status": "planned", + "sections": [], + "homepageEyebrow": "", + "showOnHomepage": false, + "showNewTag": false, + "newTagLabel": "NEW", + "numberOfChapters": 1 + } + ], + "colossiansStudySections": [ + { + "id": "1-1-2", + "chapter": 1, + "reference": "1:1-2", + "title": "Paul’s Greeting", + "audioEmbedUrl": "", + "passageText": "v. 1 Paul, an apostle of Christ Jesus by the will of God, and Timothy our brother,\n\nv. 2 To the saints in Colossae, the faithful brothers in Christ: Grace and peace to you from God our Father.", + "summary": "Paul opens with apostolic authority and warm fellowship, greeting the saints at Colossae with the twin gifts that frame every believer's life: grace and peace.", + "commentary": "Paul opens with his credentials: an apostle of Christ Jesus. But the manner of the claim matters - he immediately attributes this not to his own merit but to the will of God. This is the same humility that runs beneath the letter's entire argument: everything comes from God, not from human achievement. Timothy is included not as a co-author but as a fellow laborer, identified simply as 'our brother' - the relational warmth is immediate. The recipients are addressed as 'saints' - the Greek hagios means holy ones, set apart by God - and as 'faithful brothers in Christ.' These are not terms of flattery but of theological definition: these are people who belong to God. Paul's greeting, 'grace and peace,' is more than convention. Grace (charis) is God's unmerited favor; peace (eirene) is the wholeness and well-being that flows from it. Every believer's life is framed by these two realities.", + "greekNotes": [ + "Apostle (apostolos): one commissioned directly by Jesus Christ, invested with authority to speak on His behalf.", + "Will (thelema): desire or purpose; Paul was not self-appointed but called by God’s initiative.", + "Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.", + "Saints (hagios): God’s holy people set apart as belonging to Him, not holy by their own merit.", + "Grace (charis): God's unmerited favor that initiates and sustains the believer's life.", + "Peace (eirene): wholeness and well-being that flows from reconciliation with God." + ], + "studyQuestions": [ + "Why does Paul begin with grace and peace?", + "What tone does this create for the rest of the study?" + ] + }, + { + "id": "1-3-8", + "chapter": 1, + "reference": "1:3-8", + "title": "Thanksgiving and Report", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul thanks God for faith, hope, and love, and reports that the gospel is bearing fruit.", + "commentary": "The gospel is shown to be active and alive. Paul points to evidence of real spiritual growth, not merely religious activity.", + "greekNotes": [], + "studyQuestions": [ + "What signs of gospel growth does Paul mention?", + "How do faith, hope, and love work together here?" + ] + }, + { + "id": "1-9-14", + "chapter": 1, + "reference": "1:9-14", + "title": "Paul’s Prayer", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul prays for spiritual wisdom, endurance, and a life worthy of the Lord.", + "commentary": "This prayer shows that knowledge and fruitfulness belong together. Paul wants the believers to know God’s will and walk in a way that reflects it.", + "greekNotes": [], + "studyQuestions": [ + "What does Paul ask God to produce?", + "How does this prayer shape the goals of the study?" + ] + }, + { + "id": "1-15-20", + "chapter": 1, + "reference": "1:15-20", + "title": "The Supremacy of Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Christ is supreme over creation, the church, and reconciliation.", + "commentary": "This is the center of Colossians. Everything else in the letter depends on who Christ is and what He has done.", + "greekNotes": [], + "studyQuestions": [ + "What stands out most about Christ’s supremacy?", + "Why is this passage central to the study?" + ] + }, + { + "id": "1-21-23", + "chapter": 1, + "reference": "1:21-23", + "title": "Reconciliation Applied", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains how Christ’s work changes alienation into steadfast faith.", + "commentary": "The gospel moves from doctrine to real life. Paul shows what reconciliation looks like for people who have been brought near to God.", + "greekNotes": [], + "studyQuestions": [ + "How does Paul describe the believer’s former condition?", + "What does continuing in the faith look like?" + ] + }, + { + "id": "1-24-29", + "chapter": 1, + "reference": "1:24-29", + "title": "Paul’s Ministry and the Revealed Mystery", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.", + "commentary": "Paul’s ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.", + "greekNotes": [], + "studyQuestions": [ + "What is the goal of Paul’s ministry?", + "Why is maturity such an important theme here?" + ] + }, + { + "id": "2-1-5", + "chapter": 2, + "reference": "2:1-5", + "title": "Paul’s Pastoral Concern", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reveals his struggle for the believers and his desire for encouragement and unity.", + "commentary": "This passage shows the heart behind the letter. Paul is deeply invested in the spiritual stability of the church.", + "greekNotes": [], + "studyQuestions": [ + "What does Paul want the believers to experience?", + "Why does he emphasize encouragement and unity?" + ] + }, + { + "id": "2-6-8", + "chapter": 2, + "reference": "2:6-8", + "title": "Walk in Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to continue in Christ and resist deceptive teaching.", + "commentary": "The Christian life begins in Christ and must continue in Christ. Paul warns that subtle deception can slowly pull believers off course.", + "greekNotes": [], + "studyQuestions": [ + "What does it mean to continue in Christ?", + "What kinds of deception should believers watch for?" + ] + }, + { + "id": "2-9-10", + "chapter": 2, + "reference": "2:9-10", + "title": "Supremacy and Sufficiency", + "audioEmbedUrl": "", + "passageText": "", + "summary": "The fullness of deity dwells in Christ, and believers are made complete in Him.", + "commentary": "This section corrects the idea that anything must be added to Christ. He is fully sufficient for the believer’s life and growth.", + "greekNotes": [], + "studyQuestions": [ + "Why is fullness in Christ such an important correction?", + "How does this guard against spiritual insecurity?" + ] + }, + { + "id": "2-11-15", + "chapter": 2, + "reference": "2:11-15", + "title": "What Christ Has Done", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains the believer’s union with Christ in His death, resurrection, and victory.", + "commentary": "The work of Christ is presented as decisive and complete. This becomes the foundation for all the commands that follow.", + "greekNotes": [], + "studyQuestions": [ + "What changes because of Christ’s work here?", + "How does victory over the powers encourage believers today?" + ] + }, + { + "id": "2-16-17", + "chapter": 2, + "reference": "2:16-17", + "title": "Freedom from Legalistic Judgment", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against letting others judge believers over shadows when Christ is the substance.", + "commentary": "Outward practices can never replace Christ. Paul reminds the church that the real thing has come in Jesus.", + "greekNotes": [], + "studyQuestions": [ + "What do shadows and substance mean here?", + "Where do believers still feel pressure from external judgment?" + ] + }, + { + "id": "2-18-19", + "chapter": 2, + "reference": "2:18-19", + "title": "Warning Against False Mysticism", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against prideful spirituality that disconnects believers from Christ the head.", + "commentary": "Anything that pulls attention away from Christ and His body is spiritually dangerous. True growth stays connected to the head.", + "greekNotes": [], + "studyQuestions": [ + "How does Paul describe false spirituality?", + "Why is connection to Christ essential?" + ] + }, + { + "id": "2-20-23", + "chapter": 2, + "reference": "2:20-23", + "title": "The Emptiness of Human Regulations", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Human rules cannot produce true transformation.", + "commentary": "External restrictions may look wise, but they cannot change the heart. Paul points the church back to union with Christ.", + "greekNotes": [], + "studyQuestions": [ + "Why do human regulations fail to transform the heart?", + "What does real spiritual change require?" + ] + }, + { + "id": "3-1-4", + "chapter": 3, + "reference": "3:1-4", + "title": "Seek the Things Above", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to set their hearts and minds on Christ.", + "commentary": "Identity in Christ leads to a new direction for the mind and heart. The believer’s life is hidden with Christ, so priorities shift upward.", + "greekNotes": [], + "studyQuestions": [ + "What does it mean to seek the things above?", + "How does hidden life in Christ shape daily priorities?" + ] + }, + { + "id": "3-5-9", + "chapter": 3, + "reference": "3:5-9", + "title": "Put to Death the Old Life", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to put away the practices of the old life.", + "commentary": "Christian growth includes real moral change. Paul’s language is strong because the old life cannot be carried into the new one.", + "greekNotes": [], + "studyQuestions": [ + "Which old patterns does Paul name?", + "Why is decisive change necessary?" + ] + }, + { + "id": "3-10-11", + "chapter": 3, + "reference": "3:10-11", + "title": "Put On the New Self", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Believers are renewed in Christ and formed into one new people.", + "commentary": "Renewal in Christ changes both personal identity and community life. The new self is not merely private; it is shared by the body of Christ.", + "greekNotes": [], + "studyQuestions": [ + "What does renewal in Christ produce?", + "How does this reshape how believers see one another?" + ] + }, + { + "id": "3-12-14", + "chapter": 3, + "reference": "3:12-14", + "title": "Clothe Yourselves with Christlike Virtues", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul lists the virtues believers should wear as Christ’s character becomes visible in them.", + "commentary": "The image of clothing makes virtue practical and visible. Paul wants Christ’s character to define the way believers live together.", + "greekNotes": [], + "studyQuestions": [ + "Which virtues are most needed in your context?", + "How does love bind these virtues together?" + ] + }, + { + "id": "3-15-17", + "chapter": 3, + "reference": "3:15-17", + "title": "The Rule of Christ’s Peace and Word", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls the church to let Christ’s peace rule and His word dwell richly among them.", + "commentary": "Peace, gratitude, and the word of Christ shape a healthy Christian community. These verses connect worship, teaching, and daily life.", + "greekNotes": [], + "studyQuestions": [ + "What does it look like for Christ’s peace to rule?", + "How can the word of Christ dwell richly in a church family?" + ] + }, + { + "id": "3-18-21", + "chapter": 3, + "reference": "3:18-21", + "title": "Household Code — Relationships", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul applies the new life in Christ to family relationships.", + "commentary": "The gospel reaches into ordinary relationships. Christ changes how homes are ordered and how people treat one another.", + "greekNotes": [], + "studyQuestions": [ + "How do these instructions reflect Christ’s lordship?", + "Which relationship is most challenging to apply today?" + ] + }, + { + "id": "3-22-25", + "chapter": 3, + "reference": "3:22-25", + "title": "Household Code — Work as unto the Lord", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul instructs believers to serve faithfully and wholeheartedly.", + "commentary": "Work becomes worship when it is done for Christ. Paul teaches integrity, diligence, and accountability before a heavenly Master.", + "greekNotes": [], + "studyQuestions": [ + "How does serving the Lord reshape everyday work?", + "What does Paul say about fairness and accountability?" + ] + }, + { + "id": "4-1", + "chapter": 4, + "reference": "4:1", + "title": "Masters — Fairness Before a Heavenly Master", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reminds masters to act justly because they too answer to the Lord.", + "commentary": "The gospel confronts authority as well as submission. All authority is accountable to Christ.", + "greekNotes": [], + "studyQuestions": [ + "Why does Paul remind masters of their heavenly Master?", + "What does fairness look like in this context?" + ] + }, + { + "id": "4-2-6", + "chapter": 4, + "reference": "4:2-6", + "title": "Devoted to Prayer and Wise Witness", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul closes with a call to steadfast prayer and wise witness toward outsiders.", + "commentary": "Prayer and witness belong together. Paul shows that spiritual alertness and everyday speech are both part of faithful ministry.", + "greekNotes": [], + "studyQuestions": [ + "How are prayer and witness connected here?", + "What does gracious speech look like in real life?" + ] + }, + { + "id": "4-7-9", + "chapter": 4, + "reference": "4:7-9", + "title": "Tychicus and Onesimus", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul sends trusted messengers to encourage the church and bring news from the ministry field.", + "commentary": "These greetings reveal the relational side of ministry. The letter is carried by real people who serve the church with loyalty and care.", + "greekNotes": [], + "studyQuestions": [ + "Why do these messengers matter to the letter?", + "What does this tell us about trusted coworkers?" + ] + }, + { + "id": "4-10-14", + "chapter": 4, + "reference": "4:10-14", + "title": "Greetings from Paul’s Co-workers", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul names the coworkers who are with him and commends their ministry support.", + "commentary": "The closing greetings show the breadth of the gospel network. Ministry is shared, supported, and strengthened by faithful co-workers.", + "greekNotes": [], + "studyQuestions": [ + "What stands out about Paul’s coworkers?", + "How does shared ministry strengthen the church?" + ] + }, + { + "id": "4-15-18", + "chapter": 4, + "reference": "4:15-18", + "title": "Final Instructions and Closing", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul offers final greetings, instructions, and a closing reminder to complete the ministry entrusted to them.", + "commentary": "The letter ends with community, responsibility, and grace. Paul’s final words keep the church focused on the mission it has received.", + "greekNotes": [], + "studyQuestions": [ + "What final responsibility does Paul place on the church?", + "How does the closing reinforce the letter’s themes?" + ] + } + ], "shareHeading": "Help one more person hear the Word this week.", "shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.", "whereToNextEyebrow": "Where to Next", @@ -183,5 +1001,5 @@ ] } }, - "updatedAt": "2026-05-07T15:35:23.888Z" + "updatedAt": "2026-05-12T16:46:21.985Z" } \ No newline at end of file diff --git a/data/admin-content.json b/data/admin-content.json index c9026c0..4c9aa14 100644 --- a/data/admin-content.json +++ b/data/admin-content.json @@ -48,6 +48,814 @@ "studyGuideDownloadUrl": "", "studyGuideUrl": "https://a.co/d/01sG2tOJ", "studyGuideAmazonButtonLabel": "Get it on Amazon", + "studies": [ + { + "id": "study-colossians", + "slug": "colossians", + "title": "Colossians: Rooted in Christ", + "description": "Walk through Colossians in guided lessons with commentary, Greek notes, and discussion prompts.", + "status": "active", + "sections": [ + { + "id": "1-1-2", + "chapter": 1, + "reference": "1:1-2", + "title": "Paul’s Greeting", + "audioEmbedUrl": "", + "passageText": "v. 1 Paul, an apostle of Christ Jesus by the will of God, and Timothy our brother,\n\nv. 2 To the saints in Colossae, the faithful brothers in Christ: Grace and peace to you from God our Father.", + "summary": "Paul opens with apostolic authority and warm fellowship, greeting the saints at Colossae with the twin gifts that frame every believer's life: grace and peace.", + "commentary": "Paul opens with his credentials: an apostle of Christ Jesus. But the manner of the claim matters - he immediately attributes this not to his own merit but to the will of God. This is the same humility that runs beneath the letter's entire argument: everything comes from God, not from human achievement. Timothy is included not as a co-author but as a fellow laborer, identified simply as 'our brother' - the relational warmth is immediate. The recipients are addressed as 'saints' - the Greek hagios means holy ones, set apart by God - and as 'faithful brothers in Christ.' These are not terms of flattery but of theological definition: these are people who belong to God. Paul's greeting, 'grace and peace,' is more than convention. Grace (charis) is God's unmerited favor; peace (eirene) is the wholeness and well-being that flows from it. Every believer's life is framed by these two realities.", + "studyQuestions": [ + "Why does Paul begin with grace and peace?", + "What tone does this create for the rest of the study?" + ], + "greekNotes": [ + "Apostle (apostolos): one commissioned directly by Jesus Christ, invested with authority to speak on His behalf.", + "Will (thelema): desire or purpose; Paul was not self-appointed but called by God’s initiative.", + "Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.", + "Saints (hagios): God’s holy people set apart as belonging to Him, not holy by their own merit.", + "Grace (charis): God's unmerited favor that initiates and sustains the believer's life.", + "Peace (eirene): wholeness and well-being that flows from reconciliation with God." + ], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "1-3-8", + "chapter": 1, + "reference": "1:3-8", + "title": "Thanksgiving and Report", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul thanks God for faith, hope, and love, and reports that the gospel is bearing fruit.", + "commentary": "The gospel is shown to be active and alive. Paul points to evidence of real spiritual growth, not merely religious activity.", + "studyQuestions": [ + "What signs of gospel growth does Paul mention?", + "How do faith, hope, and love work together here?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "1-9-14", + "chapter": 1, + "reference": "1:9-14", + "title": "Paul’s Prayer", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul prays for spiritual wisdom, endurance, and a life worthy of the Lord.", + "commentary": "This prayer shows that knowledge and fruitfulness belong together. Paul wants the believers to know God’s will and walk in a way that reflects it.", + "studyQuestions": [ + "What does Paul ask God to produce?", + "How does this prayer shape the goals of the study?" + ], + "greekNotes": [] + }, + { + "id": "1-15-20", + "chapter": 1, + "reference": "1:15-20", + "title": "The Supremacy of Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Christ is supreme over creation, the church, and reconciliation.", + "commentary": "This is the center of Colossians. Everything else in the letter depends on who Christ is and what He has done.", + "studyQuestions": [ + "What stands out most about Christ’s supremacy?", + "Why is this passage central to the study?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "1-21-23", + "chapter": 1, + "reference": "1:21-23", + "title": "Reconciliation Applied", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains how Christ’s work changes alienation into steadfast faith.", + "commentary": "The gospel moves from doctrine to real life. Paul shows what reconciliation looks like for people who have been brought near to God.", + "studyQuestions": [ + "How does Paul describe the believer’s former condition?", + "What does continuing in the faith look like?" + ], + "greekNotes": [], + "releasedAt": "2026-05-18T00:00:00Z" + }, + { + "id": "1-24-29", + "chapter": 1, + "reference": "1:24-29", + "title": "Paul’s Ministry and the Revealed Mystery", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.", + "commentary": "Paul’s ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.", + "studyQuestions": [ + "What is the goal of Paul’s ministry?", + "Why is maturity such an important theme here?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-1-5", + "chapter": 2, + "reference": "2:1-5", + "title": "Paul’s Pastoral Concern", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reveals his struggle for the believers and his desire for encouragement and unity.", + "commentary": "This passage shows the heart behind the letter. Paul is deeply invested in the spiritual stability of the church.", + "studyQuestions": [ + "What does Paul want the believers to experience?", + "Why does he emphasize encouragement and unity?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-6-8", + "chapter": 2, + "reference": "2:6-8", + "title": "Walk in Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to continue in Christ and resist deceptive teaching.", + "commentary": "The Christian life begins in Christ and must continue in Christ. Paul warns that subtle deception can slowly pull believers off course.", + "studyQuestions": [ + "What does it mean to continue in Christ?", + "What kinds of deception should believers watch for?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-9-10", + "chapter": 2, + "reference": "2:9-10", + "title": "Supremacy and Sufficiency", + "audioEmbedUrl": "", + "passageText": "", + "summary": "The fullness of deity dwells in Christ, and believers are made complete in Him.", + "commentary": "This section corrects the idea that anything must be added to Christ. He is fully sufficient for the believer’s life and growth.", + "studyQuestions": [ + "Why is fullness in Christ such an important correction?", + "How does this guard against spiritual insecurity?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-11-15", + "chapter": 2, + "reference": "2:11-15", + "title": "What Christ Has Done", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains the believer’s union with Christ in His death, resurrection, and victory.", + "commentary": "The work of Christ is presented as decisive and complete. This becomes the foundation for all the commands that follow.", + "studyQuestions": [ + "What changes because of Christ’s work here?", + "How does victory over the powers encourage believers today?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-16-17", + "chapter": 2, + "reference": "2:16-17", + "title": "Freedom from Legalistic Judgment", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against letting others judge believers over shadows when Christ is the substance.", + "commentary": "Outward practices can never replace Christ. Paul reminds the church that the real thing has come in Jesus.", + "studyQuestions": [ + "What do shadows and substance mean here?", + "Where do believers still feel pressure from external judgment?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-18-19", + "chapter": 2, + "reference": "2:18-19", + "title": "Warning Against False Mysticism", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against prideful spirituality that disconnects believers from Christ the head.", + "commentary": "Anything that pulls attention away from Christ and His body is spiritually dangerous. True growth stays connected to the head.", + "studyQuestions": [ + "How does Paul describe false spirituality?", + "Why is connection to Christ essential?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "2-20-23", + "chapter": 2, + "reference": "2:20-23", + "title": "The Emptiness of Human Regulations", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Human rules cannot produce true transformation.", + "commentary": "External restrictions may look wise, but they cannot change the heart. Paul points the church back to union with Christ.", + "studyQuestions": [ + "Why do human regulations fail to transform the heart?", + "What does real spiritual change require?" + ], + "greekNotes": [], + "releasedAt": "2026-06-01T00:00:00Z" + }, + { + "id": "3-1-4", + "chapter": 3, + "reference": "3:1-4", + "title": "Seek the Things Above", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to set their hearts and minds on Christ.", + "commentary": "Identity in Christ leads to a new direction for the mind and heart. The believer’s life is hidden with Christ, so priorities shift upward.", + "studyQuestions": [ + "What does it mean to seek the things above?", + "How does hidden life in Christ shape daily priorities?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-5-9", + "chapter": 3, + "reference": "3:5-9", + "title": "Put to Death the Old Life", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to put away the practices of the old life.", + "commentary": "Christian growth includes real moral change. Paul’s language is strong because the old life cannot be carried into the new one.", + "studyQuestions": [ + "Which old patterns does Paul name?", + "Why is decisive change necessary?" + ], + "greekNotes": [], + "releasedAt": "2026-06-01T00:00:00Z" + }, + { + "id": "3-10-11", + "chapter": 3, + "reference": "3:10-11", + "title": "Put On the New Self", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Believers are renewed in Christ and formed into one new people.", + "commentary": "Renewal in Christ changes both personal identity and community life. The new self is not merely private; it is shared by the body of Christ.", + "studyQuestions": [ + "What does renewal in Christ produce?", + "How does this reshape how believers see one another?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-12-14", + "chapter": 3, + "reference": "3:12-14", + "title": "Clothe Yourselves with Christlike Virtues", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul lists the virtues believers should wear as Christ’s character becomes visible in them.", + "commentary": "The image of clothing makes virtue practical and visible. Paul wants Christ’s character to define the way believers live together.", + "studyQuestions": [ + "Which virtues are most needed in your context?", + "How does love bind these virtues together?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-15-17", + "chapter": 3, + "reference": "3:15-17", + "title": "The Rule of Christ’s Peace and Word", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls the church to let Christ’s peace rule and His word dwell richly among them.", + "commentary": "Peace, gratitude, and the word of Christ shape a healthy Christian community. These verses connect worship, teaching, and daily life.", + "studyQuestions": [ + "What does it look like for Christ’s peace to rule?", + "How can the word of Christ dwell richly in a church family?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-18-21", + "chapter": 3, + "reference": "3:18-21", + "title": "Household Code — Relationships", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul applies the new life in Christ to family relationships.", + "commentary": "The gospel reaches into ordinary relationships. Christ changes how homes are ordered and how people treat one another.", + "studyQuestions": [ + "How do these instructions reflect Christ’s lordship?", + "Which relationship is most challenging to apply today?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "3-22-25", + "chapter": 3, + "reference": "3:22-25", + "title": "Household Code — Work as unto the Lord", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul instructs believers to serve faithfully and wholeheartedly.", + "commentary": "Work becomes worship when it is done for Christ. Paul teaches integrity, diligence, and accountability before a heavenly Master.", + "studyQuestions": [ + "How does serving the Lord reshape everyday work?", + "What does Paul say about fairness and accountability?" + ], + "greekNotes": [] + }, + { + "id": "4-1", + "chapter": 4, + "reference": "4:1", + "title": "Masters — Fairness Before a Heavenly Master", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reminds masters to act justly because they too answer to the Lord.", + "commentary": "The gospel confronts authority as well as submission. All authority is accountable to Christ.", + "studyQuestions": [ + "Why does Paul remind masters of their heavenly Master?", + "What does fairness look like in this context?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-2-6", + "chapter": 4, + "reference": "4:2-6", + "title": "Devoted to Prayer and Wise Witness", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul closes with a call to steadfast prayer and wise witness toward outsiders.", + "commentary": "Prayer and witness belong together. Paul shows that spiritual alertness and everyday speech are both part of faithful ministry.", + "studyQuestions": [ + "How are prayer and witness connected here?", + "What does gracious speech look like in real life?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-7-9", + "chapter": 4, + "reference": "4:7-9", + "title": "Tychicus and Onesimus", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul sends trusted messengers to encourage the church and bring news from the ministry field.", + "commentary": "These greetings reveal the relational side of ministry. The letter is carried by real people who serve the church with loyalty and care.", + "studyQuestions": [ + "Why do these messengers matter to the letter?", + "What does this tell us about trusted coworkers?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-10-14", + "chapter": 4, + "reference": "4:10-14", + "title": "Greetings from Paul’s Co-workers", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul names the coworkers who are with him and commends their ministry support.", + "commentary": "The closing greetings show the breadth of the gospel network. Ministry is shared, supported, and strengthened by faithful co-workers.", + "studyQuestions": [ + "What stands out about Paul’s coworkers?", + "How does shared ministry strengthen the church?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + }, + { + "id": "4-15-18", + "chapter": 4, + "reference": "4:15-18", + "title": "Final Instructions and Closing", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul offers final greetings, instructions, and a closing reminder to complete the ministry entrusted to them.", + "commentary": "The letter ends with community, responsibility, and grace. Paul’s final words keep the church focused on the mission it has received.", + "studyQuestions": [ + "What final responsibility does Paul place on the church?", + "How does the closing reinforce the letter’s themes?" + ], + "greekNotes": [], + "releasedAt": "2026-05-04T00:00:00Z" + } + ] + }, + { + "id": "study-titus", + "slug": "titus", + "title": "Titus: Sound Doctrine", + "description": "A full online class version of Titus with lesson-by-lesson notes and discussion.", + "status": "planned", + "sections": [] + } + ], + "colossiansStudySections": [ + { + "id": "1-1-2", + "chapter": 1, + "reference": "1:1-2", + "title": "Paul’s Greeting", + "audioEmbedUrl": "", + "passageText": "v. 1 Paul, an apostle of Christ Jesus by the will of God, and Timothy our brother,\n\nv. 2 To the saints in Colossae, the faithful brothers in Christ: Grace and peace to you from God our Father.", + "summary": "Paul opens with apostolic authority and warm fellowship, greeting the saints at Colossae with the twin gifts that frame every believer's life: grace and peace.", + "commentary": "Paul opens with his credentials: an apostle of Christ Jesus. But the manner of the claim matters - he immediately attributes this not to his own merit but to the will of God. This is the same humility that runs beneath the letter's entire argument: everything comes from God, not from human achievement. Timothy is included not as a co-author but as a fellow laborer, identified simply as 'our brother' - the relational warmth is immediate. The recipients are addressed as 'saints' - the Greek hagios means holy ones, set apart by God - and as 'faithful brothers in Christ.' These are not terms of flattery but of theological definition: these are people who belong to God. Paul's greeting, 'grace and peace,' is more than convention. Grace (charis) is God's unmerited favor; peace (eirene) is the wholeness and well-being that flows from it. Every believer's life is framed by these two realities.", + "greekNotes": [ + "Apostle (apostolos): one commissioned directly by Jesus Christ, invested with authority to speak on His behalf.", + "Will (thelema): desire or purpose; Paul was not self-appointed but called by God’s initiative.", + "Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.", + "Saints (hagios): God’s holy people set apart as belonging to Him, not holy by their own merit.", + "Grace (charis): God's unmerited favor that initiates and sustains the believer's life.", + "Peace (eirene): wholeness and well-being that flows from reconciliation with God." + ], + "studyQuestions": [ + "Why does Paul begin with grace and peace?", + "What tone does this create for the rest of the study?" + ] + }, + { + "id": "1-3-8", + "chapter": 1, + "reference": "1:3-8", + "title": "Thanksgiving and Report", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul thanks God for faith, hope, and love, and reports that the gospel is bearing fruit.", + "commentary": "The gospel is shown to be active and alive. Paul points to evidence of real spiritual growth, not merely religious activity.", + "greekNotes": [], + "studyQuestions": [ + "What signs of gospel growth does Paul mention?", + "How do faith, hope, and love work together here?" + ] + }, + { + "id": "1-9-14", + "chapter": 1, + "reference": "1:9-14", + "title": "Paul’s Prayer", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul prays for spiritual wisdom, endurance, and a life worthy of the Lord.", + "commentary": "This prayer shows that knowledge and fruitfulness belong together. Paul wants the believers to know God’s will and walk in a way that reflects it.", + "greekNotes": [], + "studyQuestions": [ + "What does Paul ask God to produce?", + "How does this prayer shape the goals of the study?" + ] + }, + { + "id": "1-15-20", + "chapter": 1, + "reference": "1:15-20", + "title": "The Supremacy of Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Christ is supreme over creation, the church, and reconciliation.", + "commentary": "This is the center of Colossians. Everything else in the letter depends on who Christ is and what He has done.", + "greekNotes": [], + "studyQuestions": [ + "What stands out most about Christ’s supremacy?", + "Why is this passage central to the study?" + ] + }, + { + "id": "1-21-23", + "chapter": 1, + "reference": "1:21-23", + "title": "Reconciliation Applied", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains how Christ’s work changes alienation into steadfast faith.", + "commentary": "The gospel moves from doctrine to real life. Paul shows what reconciliation looks like for people who have been brought near to God.", + "greekNotes": [], + "studyQuestions": [ + "How does Paul describe the believer’s former condition?", + "What does continuing in the faith look like?" + ] + }, + { + "id": "1-24-29", + "chapter": 1, + "reference": "1:24-29", + "title": "Paul’s Ministry and the Revealed Mystery", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.", + "commentary": "Paul’s ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.", + "greekNotes": [], + "studyQuestions": [ + "What is the goal of Paul’s ministry?", + "Why is maturity such an important theme here?" + ] + }, + { + "id": "2-1-5", + "chapter": 2, + "reference": "2:1-5", + "title": "Paul’s Pastoral Concern", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reveals his struggle for the believers and his desire for encouragement and unity.", + "commentary": "This passage shows the heart behind the letter. Paul is deeply invested in the spiritual stability of the church.", + "greekNotes": [], + "studyQuestions": [ + "What does Paul want the believers to experience?", + "Why does he emphasize encouragement and unity?" + ] + }, + { + "id": "2-6-8", + "chapter": 2, + "reference": "2:6-8", + "title": "Walk in Christ", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to continue in Christ and resist deceptive teaching.", + "commentary": "The Christian life begins in Christ and must continue in Christ. Paul warns that subtle deception can slowly pull believers off course.", + "greekNotes": [], + "studyQuestions": [ + "What does it mean to continue in Christ?", + "What kinds of deception should believers watch for?" + ] + }, + { + "id": "2-9-10", + "chapter": 2, + "reference": "2:9-10", + "title": "Supremacy and Sufficiency", + "audioEmbedUrl": "", + "passageText": "", + "summary": "The fullness of deity dwells in Christ, and believers are made complete in Him.", + "commentary": "This section corrects the idea that anything must be added to Christ. He is fully sufficient for the believer’s life and growth.", + "greekNotes": [], + "studyQuestions": [ + "Why is fullness in Christ such an important correction?", + "How does this guard against spiritual insecurity?" + ] + }, + { + "id": "2-11-15", + "chapter": 2, + "reference": "2:11-15", + "title": "What Christ Has Done", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul explains the believer’s union with Christ in His death, resurrection, and victory.", + "commentary": "The work of Christ is presented as decisive and complete. This becomes the foundation for all the commands that follow.", + "greekNotes": [], + "studyQuestions": [ + "What changes because of Christ’s work here?", + "How does victory over the powers encourage believers today?" + ] + }, + { + "id": "2-16-17", + "chapter": 2, + "reference": "2:16-17", + "title": "Freedom from Legalistic Judgment", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against letting others judge believers over shadows when Christ is the substance.", + "commentary": "Outward practices can never replace Christ. Paul reminds the church that the real thing has come in Jesus.", + "greekNotes": [], + "studyQuestions": [ + "What do shadows and substance mean here?", + "Where do believers still feel pressure from external judgment?" + ] + }, + { + "id": "2-18-19", + "chapter": 2, + "reference": "2:18-19", + "title": "Warning Against False Mysticism", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul warns against prideful spirituality that disconnects believers from Christ the head.", + "commentary": "Anything that pulls attention away from Christ and His body is spiritually dangerous. True growth stays connected to the head.", + "greekNotes": [], + "studyQuestions": [ + "How does Paul describe false spirituality?", + "Why is connection to Christ essential?" + ] + }, + { + "id": "2-20-23", + "chapter": 2, + "reference": "2:20-23", + "title": "The Emptiness of Human Regulations", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Human rules cannot produce true transformation.", + "commentary": "External restrictions may look wise, but they cannot change the heart. Paul points the church back to union with Christ.", + "greekNotes": [], + "studyQuestions": [ + "Why do human regulations fail to transform the heart?", + "What does real spiritual change require?" + ] + }, + { + "id": "3-1-4", + "chapter": 3, + "reference": "3:1-4", + "title": "Seek the Things Above", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to set their hearts and minds on Christ.", + "commentary": "Identity in Christ leads to a new direction for the mind and heart. The believer’s life is hidden with Christ, so priorities shift upward.", + "greekNotes": [], + "studyQuestions": [ + "What does it mean to seek the things above?", + "How does hidden life in Christ shape daily priorities?" + ] + }, + { + "id": "3-5-9", + "chapter": 3, + "reference": "3:5-9", + "title": "Put to Death the Old Life", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls believers to put away the practices of the old life.", + "commentary": "Christian growth includes real moral change. Paul’s language is strong because the old life cannot be carried into the new one.", + "greekNotes": [], + "studyQuestions": [ + "Which old patterns does Paul name?", + "Why is decisive change necessary?" + ] + }, + { + "id": "3-10-11", + "chapter": 3, + "reference": "3:10-11", + "title": "Put On the New Self", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Believers are renewed in Christ and formed into one new people.", + "commentary": "Renewal in Christ changes both personal identity and community life. The new self is not merely private; it is shared by the body of Christ.", + "greekNotes": [], + "studyQuestions": [ + "What does renewal in Christ produce?", + "How does this reshape how believers see one another?" + ] + }, + { + "id": "3-12-14", + "chapter": 3, + "reference": "3:12-14", + "title": "Clothe Yourselves with Christlike Virtues", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul lists the virtues believers should wear as Christ’s character becomes visible in them.", + "commentary": "The image of clothing makes virtue practical and visible. Paul wants Christ’s character to define the way believers live together.", + "greekNotes": [], + "studyQuestions": [ + "Which virtues are most needed in your context?", + "How does love bind these virtues together?" + ] + }, + { + "id": "3-15-17", + "chapter": 3, + "reference": "3:15-17", + "title": "The Rule of Christ’s Peace and Word", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul calls the church to let Christ’s peace rule and His word dwell richly among them.", + "commentary": "Peace, gratitude, and the word of Christ shape a healthy Christian community. These verses connect worship, teaching, and daily life.", + "greekNotes": [], + "studyQuestions": [ + "What does it look like for Christ’s peace to rule?", + "How can the word of Christ dwell richly in a church family?" + ] + }, + { + "id": "3-18-21", + "chapter": 3, + "reference": "3:18-21", + "title": "Household Code — Relationships", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul applies the new life in Christ to family relationships.", + "commentary": "The gospel reaches into ordinary relationships. Christ changes how homes are ordered and how people treat one another.", + "greekNotes": [], + "studyQuestions": [ + "How do these instructions reflect Christ’s lordship?", + "Which relationship is most challenging to apply today?" + ] + }, + { + "id": "3-22-25", + "chapter": 3, + "reference": "3:22-25", + "title": "Household Code — Work as unto the Lord", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul instructs believers to serve faithfully and wholeheartedly.", + "commentary": "Work becomes worship when it is done for Christ. Paul teaches integrity, diligence, and accountability before a heavenly Master.", + "greekNotes": [], + "studyQuestions": [ + "How does serving the Lord reshape everyday work?", + "What does Paul say about fairness and accountability?" + ] + }, + { + "id": "4-1", + "chapter": 4, + "reference": "4:1", + "title": "Masters — Fairness Before a Heavenly Master", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul reminds masters to act justly because they too answer to the Lord.", + "commentary": "The gospel confronts authority as well as submission. All authority is accountable to Christ.", + "greekNotes": [], + "studyQuestions": [ + "Why does Paul remind masters of their heavenly Master?", + "What does fairness look like in this context?" + ] + }, + { + "id": "4-2-6", + "chapter": 4, + "reference": "4:2-6", + "title": "Devoted to Prayer and Wise Witness", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul closes with a call to steadfast prayer and wise witness toward outsiders.", + "commentary": "Prayer and witness belong together. Paul shows that spiritual alertness and everyday speech are both part of faithful ministry.", + "greekNotes": [], + "studyQuestions": [ + "How are prayer and witness connected here?", + "What does gracious speech look like in real life?" + ] + }, + { + "id": "4-7-9", + "chapter": 4, + "reference": "4:7-9", + "title": "Tychicus and Onesimus", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul sends trusted messengers to encourage the church and bring news from the ministry field.", + "commentary": "These greetings reveal the relational side of ministry. The letter is carried by real people who serve the church with loyalty and care.", + "greekNotes": [], + "studyQuestions": [ + "Why do these messengers matter to the letter?", + "What does this tell us about trusted coworkers?" + ] + }, + { + "id": "4-10-14", + "chapter": 4, + "reference": "4:10-14", + "title": "Greetings from Paul’s Co-workers", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul names the coworkers who are with him and commends their ministry support.", + "commentary": "The closing greetings show the breadth of the gospel network. Ministry is shared, supported, and strengthened by faithful co-workers.", + "greekNotes": [], + "studyQuestions": [ + "What stands out about Paul’s coworkers?", + "How does shared ministry strengthen the church?" + ] + }, + { + "id": "4-15-18", + "chapter": 4, + "reference": "4:15-18", + "title": "Final Instructions and Closing", + "audioEmbedUrl": "", + "passageText": "", + "summary": "Paul offers final greetings, instructions, and a closing reminder to complete the ministry entrusted to them.", + "commentary": "The letter ends with community, responsibility, and grace. Paul’s final words keep the church focused on the mission it has received.", + "greekNotes": [], + "studyQuestions": [ + "What final responsibility does Paul place on the church?", + "How does the closing reinforce the letter’s themes?" + ] + } + ], "shareHeading": "Help one more person hear the Word this week.", "shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.", "whereToNextEyebrow": "Where to Next", @@ -183,5 +991,5 @@ ] } }, - "updatedAt": "2026-05-07T15:35:23.896Z" -} \ No newline at end of file + "updatedAt": "2026-05-12T16:06:01.888Z" +} diff --git a/data/questions.json b/data/questions.json index 118899a..a2c59f5 100644 --- a/data/questions.json +++ b/data/questions.json @@ -98,5 +98,5 @@ "topic": "Faith & Life" } ], - "updatedAt": "2026-05-07T15:35:23.896Z" + "updatedAt": "2026-05-12T16:06:01.888Z" } \ No newline at end of file diff --git a/data/study-notes.json b/data/study-notes.json new file mode 100644 index 0000000..40e85af --- /dev/null +++ b/data/study-notes.json @@ -0,0 +1,8 @@ +{ + "notesByUser": { + "e43e7217-be69-49c6-a65d-edd0e7745884": { + "1-1-2": "Test" + } + }, + "updatedAt": "2026-05-12T15:51:54.402Z" +} \ No newline at end of file diff --git a/data/study-users.json b/data/study-users.json new file mode 100644 index 0000000..ca60ba3 --- /dev/null +++ b/data/study-users.json @@ -0,0 +1,13 @@ +{ + "users": [ + { + "id": "e43e7217-be69-49c6-a65d-edd0e7745884", + "username": "teststudent", + "passwordHash": "3311701f9972d1c6a0933d5ccd71f83c526e24fe491334f4a9cc705407e6f7e7", + "createdAt": "2026-05-12T15:48:03.143Z", + "updatedAt": "2026-05-12T15:51:17.052Z", + "lastLoginAt": "2026-05-12T15:51:17.052Z" + } + ], + "updatedAt": "2026-05-12T15:51:17.053Z" +} \ No newline at end of file diff --git a/server.js b/server.js index 96a08e2..b9296f2 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ import express from 'express' import rateLimit from 'express-rate-limit' import { mkdir, readdir, readFile, stat, unlink, writeFile } from 'node:fs/promises' -import { createHash, randomUUID } from 'node:crypto' +import { createHash, randomUUID, timingSafeEqual } from 'node:crypto' import path from 'node:path' import { fileURLToPath } from 'node:url' import { Resend } from 'resend' @@ -53,6 +53,8 @@ const VISITOR_STATS_FILE = path.join(DATA_DIR, 'visitor-stats.json') const CONTACT_SUBMISSIONS_FILE = path.join(DATA_DIR, 'contact-submissions.json') const QUESTIONS_FILE = path.join(DATA_DIR, 'questions.json') const DRAFT_QUESTIONS_FILE = path.join(DATA_DIR, 'questions-draft.json') +const STUDY_USERS_FILE = path.join(DATA_DIR, 'study-users.json') +const STUDY_NOTES_FILE = path.join(DATA_DIR, 'study-notes.json') const REPLY_TEMPLATES_FILE = path.join(DATA_DIR, 'admin-reply-templates.json') const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json') const BACKUP_DIR = path.join(DATA_DIR, 'backups') @@ -381,12 +383,21 @@ const DOWNLOAD_TOKEN_TTL_MS = 10 * 60 * 1000 const titusDownloadTokens = new Map() const MAX_QUESTIONS = 1000 +const MAX_STUDY_USERS = 5000 +const MAX_STUDY_NOTES_PER_USER = 500 +const MAX_STUDY_NOTE_LENGTH = 12000 +const STUDY_SESSION_COOKIE = 'vbn_study_session' +const STUDY_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000 let visitorStats = { ...EMPTY_VISITOR_STATS } let visitorStatsWritePromise = Promise.resolve() let contactSubmissions = [] let contactSubmissionsWritePromise = Promise.resolve() let questions = [] let questionsWritePromise = Promise.resolve() +let studyUsers = [] +let studyUsersWritePromise = Promise.resolve() +let studyNotesByUser = {} +let studyNotesWritePromise = Promise.resolve() let downloadCounts = {} let downloadCountsWritePromise = Promise.resolve() let lastVisitorStatsWrite = { ok: true, at: null, error: null } @@ -394,6 +405,7 @@ let lastHitStatsWrite = { ok: true, at: null, error: null } let lastBackupStatus = { ok: true, at: null, error: null, file: null } let lastCachePurgeStatus = { ok: true, at: null, error: null } let lastDeployHookStatus = { ok: true, at: null, error: null } +const studySessions = new Map() function sanitizeReplyTemplates(value) { if (!Array.isArray(value)) return [...DEFAULT_REPLY_TEMPLATES] @@ -1543,6 +1555,190 @@ function loadQuestionsFromDisk() { questions = [] }) } + +function cookieFlags() { + return process.env.NODE_ENV === 'production' ? '; Secure' : '' +} + +function normalizeStudyUsername(value) { + if (typeof value !== 'string') return '' + return value.trim().toLowerCase() +} + +function isValidStudyUsername(value) { + return /^[a-z0-9._-]{3,40}$/.test(value) +} + +function hashStudyPassword(password) { + return createHash('sha256').update(`study-user:${String(password)}`).digest('hex') +} + +function sanitizeStudyUsers(value) { + if (!Array.isArray(value)) return [] + + const out = [] + const seen = new Set() + + for (const item of value) { + const username = normalizeStudyUsername(item?.username) + const passwordHash = typeof item?.passwordHash === 'string' ? item.passwordHash.trim() : '' + if (!isValidStudyUsername(username) || !passwordHash || seen.has(username)) continue + seen.add(username) + out.push({ + id: typeof item?.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + username, + passwordHash, + createdAt: typeof item?.createdAt === 'string' ? item.createdAt : new Date().toISOString(), + updatedAt: typeof item?.updatedAt === 'string' ? item.updatedAt : new Date().toISOString(), + lastLoginAt: typeof item?.lastLoginAt === 'string' ? item.lastLoginAt : null, + }) + } + + return out.slice(0, MAX_STUDY_USERS) +} + +function sanitizeStudyNotes(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {} + const out = {} + + for (const [userId, entries] of Object.entries(value)) { + if (typeof userId !== 'string' || !userId.trim()) continue + if (!entries || typeof entries !== 'object' || Array.isArray(entries)) continue + + const userNotes = {} + let count = 0 + for (const [sectionId, note] of Object.entries(entries)) { + if (count >= MAX_STUDY_NOTES_PER_USER) break + if (!/^[a-z0-9-]{1,80}$/i.test(sectionId)) continue + if (typeof note !== 'string') continue + const trimmed = note.trim().slice(0, MAX_STUDY_NOTE_LENGTH) + if (!trimmed) continue + userNotes[sectionId] = trimmed + count += 1 + } + + out[userId] = userNotes + } + + return out +} + +function queueStudyUsersWrite() { + studyUsersWritePromise = studyUsersWritePromise + .then(async () => { + await mkdir(DATA_DIR, { recursive: true }) + await writeFile( + STUDY_USERS_FILE, + JSON.stringify({ users: studyUsers, updatedAt: new Date().toISOString() }, null, 2), + 'utf8', + ) + }) + .catch(err => { + console.error('[study-users] failed to write users:', err) + }) +} + +function queueStudyNotesWrite() { + studyNotesWritePromise = studyNotesWritePromise + .then(async () => { + await mkdir(DATA_DIR, { recursive: true }) + await writeFile( + STUDY_NOTES_FILE, + JSON.stringify({ notesByUser: studyNotesByUser, updatedAt: new Date().toISOString() }, null, 2), + 'utf8', + ) + }) + .catch(err => { + console.error('[study-notes] failed to write notes:', err) + }) +} + +function loadStudyUsersFromDisk() { + return readFile(STUDY_USERS_FILE, 'utf8') + .then(raw => { + const parsed = JSON.parse(raw) + const source = Array.isArray(parsed) ? parsed : parsed?.users + studyUsers = sanitizeStudyUsers(source) + }) + .catch(() => { + studyUsers = [] + }) +} + +function loadStudyNotesFromDisk() { + return readFile(STUDY_NOTES_FILE, 'utf8') + .then(raw => { + const parsed = JSON.parse(raw) + const source = parsed && typeof parsed === 'object' ? parsed.notesByUser : {} + studyNotesByUser = sanitizeStudyNotes(source) + }) + .catch(() => { + studyNotesByUser = {} + }) +} + +function findStudyUserByUsername(username) { + return studyUsers.find(user => user.username === normalizeStudyUsername(username)) +} + +function createStudySession(userId) { + const token = randomUUID() + studySessions.set(token, { userId, expiresAt: Date.now() + STUDY_SESSION_TTL_MS }) + return token +} + +function setStudySessionCookie(res, token) { + res.append( + 'Set-Cookie', + `${STUDY_SESSION_COOKIE}=${encodeURIComponent(token)}; Max-Age=${Math.floor(STUDY_SESSION_TTL_MS / 1000)}; Path=/; HttpOnly; SameSite=Lax${cookieFlags()}`, + ) +} + +function clearStudySessionCookie(res) { + res.append( + 'Set-Cookie', + `${STUDY_SESSION_COOKIE}=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax${cookieFlags()}`, + ) +} + +function getStudyUserFromRequest(req) { + const cookies = parseCookies(req.headers.cookie) + const token = cookies[STUDY_SESSION_COOKIE] + if (!token) return null + + const session = studySessions.get(token) + if (!session || session.expiresAt <= Date.now()) { + studySessions.delete(token) + return null + } + + const user = studyUsers.find(item => item.id === session.userId) + if (!user) { + studySessions.delete(token) + return null + } + + session.expiresAt = Date.now() + STUDY_SESSION_TTL_MS + studySessions.set(token, session) + return user +} + +function requireStudyAuth(req, res, next) { + const user = getStudyUserFromRequest(req) + if (!user) { + res.status(401).json({ message: 'Please sign in to save notes.' }) + return + } + req.studyUser = user + next() +} + +function normalizeLessonSectionId(value) { + if (typeof value !== 'string') return '' + const trimmed = value.trim().toLowerCase() + return /^[a-z0-9-]{1,80}$/.test(trimmed) ? trimmed : '' +} + // Rate limiter: max 10 attempts per 15 minutes per IP on the login endpoint const loginRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, @@ -1553,6 +1749,146 @@ const loginRateLimiter = rateLimit({ skipSuccessfulRequests: true, }) +const studyAuthRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + standardHeaders: true, + legacyHeaders: false, + message: { message: 'Too many attempts. Please wait 15 minutes and try again.' }, + skipSuccessfulRequests: true, +}) + +app.get('/api/study-auth/status', (req, res) => { + const user = getStudyUserFromRequest(req) + res.json({ authenticated: Boolean(user), username: user?.username ?? '' }) +}) + +app.post('/api/study-auth/signup', studyAuthRateLimiter, (req, res) => { + const username = normalizeStudyUsername(req.body?.username) + const password = typeof req.body?.password === 'string' ? req.body.password : '' + + if (!isValidStudyUsername(username)) { + res.status(400).json({ message: 'Username must be 3-40 characters (letters, numbers, dot, underscore, dash).' }) + return + } + + if (typeof password !== 'string' || password.length < 8 || password.length > 200) { + res.status(400).json({ message: 'Password must be 8-200 characters.' }) + return + } + + if (findStudyUserByUsername(username)) { + res.status(409).json({ message: 'That username is already in use.' }) + return + } + + const now = new Date().toISOString() + const user = { + id: randomUUID(), + username, + passwordHash: hashStudyPassword(password), + createdAt: now, + updatedAt: now, + lastLoginAt: now, + } + + studyUsers.push(user) + if (studyUsers.length > MAX_STUDY_USERS) { + studyUsers = studyUsers.slice(studyUsers.length - MAX_STUDY_USERS) + } + queueStudyUsersWrite() + + const sessionToken = createStudySession(user.id) + setStudySessionCookie(res, sessionToken) + res.json({ ok: true, username: user.username }) +}) + +app.post('/api/study-auth/login', studyAuthRateLimiter, (req, res) => { + const username = normalizeStudyUsername(req.body?.username) + const password = typeof req.body?.password === 'string' ? req.body.password : '' + const user = findStudyUserByUsername(username) + + if (!user) { + res.status(401).json({ message: 'Invalid username or password.' }) + return + } + + const submittedHash = hashStudyPassword(password) + const expectedHash = user.passwordHash + const a = Buffer.from(submittedHash, 'utf8') + const b = Buffer.from(expectedHash, 'utf8') + if (a.length !== b.length || !timingSafeEqual(a, b)) { + res.status(401).json({ message: 'Invalid username or password.' }) + return + } + + user.lastLoginAt = new Date().toISOString() + user.updatedAt = user.lastLoginAt + queueStudyUsersWrite() + + const sessionToken = createStudySession(user.id) + setStudySessionCookie(res, sessionToken) + res.json({ ok: true, username: user.username }) +}) + +app.post('/api/study-auth/logout', (req, res) => { + const cookies = parseCookies(req.headers.cookie) + const token = cookies[STUDY_SESSION_COOKIE] + if (token) { + studySessions.delete(token) + } + clearStudySessionCookie(res) + res.json({ ok: true }) +}) + +app.get('/api/study-notes', requireStudyAuth, (req, res) => { + const user = req.studyUser + const notes = studyNotesByUser[user.id] ?? {} + res.json({ notes }) +}) + +app.get('/api/study-notes/:sectionId', requireStudyAuth, (req, res) => { + const sectionId = normalizeLessonSectionId(req.params.sectionId) + if (!sectionId) { + res.status(400).json({ message: 'Invalid section id.' }) + return + } + + const user = req.studyUser + const note = studyNotesByUser[user.id]?.[sectionId] ?? '' + res.json({ note }) +}) + +app.put('/api/study-notes/:sectionId', requireStudyAuth, (req, res) => { + const sectionId = normalizeLessonSectionId(req.params.sectionId) + if (!sectionId) { + res.status(400).json({ message: 'Invalid section id.' }) + return + } + + const user = req.studyUser + const rawNote = typeof req.body?.note === 'string' ? req.body.note : '' + const note = rawNote.trim().slice(0, MAX_STUDY_NOTE_LENGTH) + + if (!studyNotesByUser[user.id]) { + studyNotesByUser[user.id] = {} + } + + if (!note) { + delete studyNotesByUser[user.id][sectionId] + } else { + const existingCount = Object.keys(studyNotesByUser[user.id]).length + if (!studyNotesByUser[user.id][sectionId] && existingCount >= MAX_STUDY_NOTES_PER_USER) { + res.status(400).json({ message: 'Notes limit reached for this account.' }) + return + } + studyNotesByUser[user.id][sectionId] = note + } + + queueStudyNotesWrite() + res.json({ ok: true, note }) +}) + app.get('/api/admin-auth/status', async (req, res) => { res.json({ authenticated: isValidAdminSession(req), @@ -3058,6 +3394,8 @@ Promise.all([ loadReplyHistoryFromDisk(), loadQuestionsFromDisk(), loadDraftQuestionsFromDisk(), + loadStudyUsersFromDisk(), + loadStudyNotesFromDisk(), loadDownloadCountsFromDisk(), refreshContentCaches(), ]) diff --git a/server/helpers.js b/server/helpers.js index ad17b09..ba86bac 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -225,6 +225,53 @@ function sanitizeArchivedSeriesNotes(value) { .filter(item => item.heading || item.body) } +function sanitizeColossiansStudySections(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => ({ + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + chapter: Number.isInteger(item.chapter) && item.chapter >= 1 && item.chapter <= 999 ? item.chapter : 1, + reference: typeof item.reference === 'string' ? item.reference.trim().slice(0, 40) : '', + title: typeof item.title === 'string' ? item.title.trim().slice(0, 160) : '', + audioEmbedUrl: sanitizeUrl(item.audioEmbedUrl), + passageText: typeof item.passageText === 'string' ? item.passageText.trim().slice(0, 12000) : '', + summary: typeof item.summary === 'string' ? item.summary.trim().slice(0, 600) : '', + commentary: typeof item.commentary === 'string' ? item.commentary.trim().slice(0, 12000) : '', + greekNotes: Array.isArray(item.greekNotes) + ? item.greekNotes.filter(note => typeof note === 'string').map(note => note.trim()).filter(Boolean).slice(0, 20) + : [], + studyQuestions: Array.isArray(item.studyQuestions) + ? item.studyQuestions.filter(question => typeof question === 'string').map(question => question.trim()).filter(Boolean).slice(0, 20) + : [], + ...(typeof item.releasedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(item.releasedAt) ? { releasedAt: item.releasedAt } : {}), + })) + .filter(item => item.title || item.summary || item.commentary || item.greekNotes.length > 0 || item.studyQuestions.length > 0 || item.passageText) +} + +function sanitizeStudies(value) { + const source = Array.isArray(value) ? value : [] + return source + .filter(item => item && typeof item === 'object') + .map(item => ({ + id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), + slug: typeof item.slug === 'string' ? item.slug.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') : '', + title: typeof item.title === 'string' ? item.title.trim().slice(0, 160) : '', + description: typeof item.description === 'string' ? item.description.trim().slice(0, 800) : '', + homepageEyebrow: typeof item.homepageEyebrow === 'string' ? item.homepageEyebrow.trim().slice(0, 80) : '', + showOnHomepage: item.showOnHomepage === true, + showNewTag: item.showNewTag === true, + newTagLabel: typeof item.newTagLabel === 'string' ? item.newTagLabel.trim().slice(0, 24) : '', + status: item.status === 'planned' ? 'planned' : 'active', + difficulty: item.difficulty === 'advanced' ? 'advanced' : (item.difficulty === 'intermediate' ? 'intermediate' : 'beginner'), + estimatedHours: Number.isFinite(Number(item.estimatedHours)) ? Math.max(1, Math.min(500, Number(item.estimatedHours))) : 8, + completionBadge: typeof item.completionBadge === 'string' ? item.completionBadge.trim().slice(0, 120) : '', + numberOfChapters: Number.isInteger(item.numberOfChapters) && item.numberOfChapters >= 1 && item.numberOfChapters <= 999 ? item.numberOfChapters : 1, + sections: sanitizeColossiansStudySections(item.sections), + })) + .filter(item => item.slug && item.title) +} + function sanitizeArchivedSeries(value) { const source = Array.isArray(value) ? value : [] return source @@ -269,6 +316,8 @@ export function sanitizeSiteContent(siteContent) { customLinks: sanitizeCustomLinks(siteContent.customLinks), customBlocks: sanitizeCustomBlocks(siteContent.customBlocks), archivedSeries: sanitizeArchivedSeries(siteContent.archivedSeries), + studies: sanitizeStudies(siteContent.studies), + colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections), redirects: sanitizeRedirectRules(siteContent.redirects), podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS), seo: { diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index e5c0eab..e60a843 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import type { ChangeEvent } from 'react' import { Link } from 'react-router-dom' -import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content' +import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content' import { DEFAULTS } from './content' import { AnalyticsPanel } from './components/AnalyticsPanel' @@ -156,13 +156,13 @@ interface ContactReplyConfig { note: string } -type StringField = Exclude +type StringField = Exclude type AdminView = | 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact' | 'current-series' | 'episode-highlights' | 'archived-series' | 'downloads' | 'custom-links' | 'content-blocks' - | 'questions' | 'analytics' | 'assets' + | 'questions' | 'analytics' | 'assets' | 'colossians-study' | 'emails' | 'subscribers' | 'contacts' | 'seo' | 'legal' | 'security' | 'brand' | 'global' @@ -287,9 +287,68 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect { key: 'welcomeEmailSignoff', label: 'Welcome Email — Signoff HTML', multiline: true, section: 'global' }, ] +function normalizeStudies(siteContent: SiteContent): StudyProgram[] { + if (Array.isArray(siteContent.studies) && siteContent.studies.length > 0) { + return siteContent.studies.map(study => ({ + ...study, + homepageEyebrow: typeof study.homepageEyebrow === 'string' ? study.homepageEyebrow : '', + showOnHomepage: study.showOnHomepage === true, + showNewTag: study.showNewTag === true, + newTagLabel: typeof study.newTagLabel === 'string' ? study.newTagLabel : 'NEW', + numberOfChapters: Number.isInteger(study.numberOfChapters) && study.numberOfChapters >= 1 && study.numberOfChapters <= 999 ? study.numberOfChapters : 1, + })) + } + + const fallbackSections = siteContent.colossiansStudySections?.length + ? siteContent.colossiansStudySections + : DEFAULTS.colossiansStudySections + + return [ + { + id: 'study-colossians', + slug: 'colossians', + title: 'Colossians: Rooted in Christ', + description: 'Walk through Colossians in guided lessons with commentary, Greek notes, and discussion prompts.', + homepageEyebrow: 'New Study', + showOnHomepage: true, + showNewTag: true, + newTagLabel: 'NEW', + status: 'active', + difficulty: 'intermediate', + estimatedHours: 12, + completionBadge: 'Colossians Completion', + numberOfChapters: 4, + sections: fallbackSections, + }, + ] +} + +function normalizeSiteContentForAdmin(siteContent: SiteContent): SiteContent { + const studies = normalizeStudies(siteContent) + const colossians = studies.find(study => study.slug === 'colossians') + + return { + ...siteContent, + studies, + colossiansStudySections: colossians?.sections?.length + ? colossians.sections + : (siteContent.colossiansStudySections?.length ? siteContent.colossiansStudySections : DEFAULTS.colossiansStudySections), + } +} + +function buildSiteContentForSave(siteContent: SiteContent): SiteContent { + const normalized = normalizeSiteContentForAdmin(siteContent) + const colossians = normalized.studies.find(study => study.slug === 'colossians') + return { + ...normalized, + colossiansStudySections: colossians?.sections ?? normalized.colossiansStudySections, + } +} + export default function AdminPage({ content, onSave, onLogout }: Props) { - const [form, setForm] = useState(content) - const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(content)) + const normalizedContent = normalizeSiteContentForAdmin(content) + const [form, setForm] = useState(normalizedContent) + const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(normalizedContent)) const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') const [errorMsg, setErrorMsg] = useState('') const [adminView, setAdminView] = useState('dashboard') @@ -368,8 +427,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { }, [form, adminView, previewOpen]) useEffect(() => { - const nextSnapshot = JSON.stringify(content) - setForm(content) + const normalized = normalizeSiteContentForAdmin(content) + const nextSnapshot = JSON.stringify(normalized) + setForm(normalized) setLastSavedSnapshot(nextSnapshot) }, [content]) @@ -575,7 +635,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { if (!r.ok) return const data = await r.json() as { siteContent?: Partial } if (data?.siteContent && typeof data.siteContent === 'object') { - const next = { ...DEFAULTS, ...data.siteContent } + const next = normalizeSiteContentForAdmin({ ...DEFAULTS, ...data.siteContent }) setForm(next) onSave(next) } @@ -601,13 +661,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { } async function handleSaveDraft() { + const payload = buildSiteContentForSave(form) setStatus('saving') setErrorMsg('') try { const res = await fetch('/api/admin-content-draft', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ siteContent: form }), + body: JSON.stringify({ siteContent: payload }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) @@ -615,7 +676,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { } const data = await res.json() as { updatedAt?: string } setPublishState(prev => ({ ...prev, draftUpdatedAt: data.updatedAt ?? new Date().toISOString() })) - setLastSavedSnapshot(JSON.stringify(form)) + setLastSavedSnapshot(JSON.stringify(payload)) setStatus('saved') setTimeout(() => setStatus('idle'), 3500) } catch (err) { @@ -626,6 +687,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { async function handlePublishDraft() { if (!confirm('Publish current changes to the live site now?')) return + const payload = buildSiteContentForSave(form) setStatus('saving') setErrorMsg('') try { @@ -633,7 +695,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { const draftRes = await fetch('/api/admin-content-draft', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ siteContent: form }), + body: JSON.stringify({ siteContent: payload }), }) if (!draftRes.ok) { const data = await draftRes.json().catch(() => ({})) @@ -653,7 +715,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { if (!latestRes.ok) throw new Error('Failed to refresh published content') const latest = await latestRes.json() as { siteContent?: Partial } if (latest?.siteContent) { - const next = { ...DEFAULTS, ...latest.siteContent } + const next = normalizeSiteContentForAdmin({ ...DEFAULTS, ...latest.siteContent }) setForm(next) onSave(next) setLastSavedSnapshot(JSON.stringify(next)) @@ -1326,6 +1388,92 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { })) } + function addStudyProgram() { + setForm(f => ({ + ...f, + studies: [ + ...(f.studies ?? []), + { + id: `study-${Date.now().toString(36)}`, + slug: `new-study-${Date.now().toString(36)}`, + title: 'New Study', + description: 'Add study description', + homepageEyebrow: 'New Study', + showOnHomepage: false, + showNewTag: false, + newTagLabel: 'NEW', + status: 'planned', + difficulty: 'beginner', + estimatedHours: 8, + completionBadge: 'Study Completion', + numberOfChapters: 4, + sections: [], + }, + ], + })) + } + + function updateStudyProgram(studyId: string, field: keyof Omit, value: string | number | boolean) { + setForm(f => ({ + ...f, + studies: (f.studies ?? []).map(study => study.id === studyId ? { ...study, [field]: value } : study), + })) + } + + function removeStudyProgram(studyId: string) { + setForm(f => ({ + ...f, + studies: (f.studies ?? []).filter(study => study.id !== studyId), + })) + } + + function addStudySection(studyId: string) { + setForm(f => ({ + ...f, + studies: (f.studies ?? []).map(study => study.id === studyId + ? { + ...study, + sections: [ + ...(study.sections ?? []), + { + id: `${study.slug || 'section'}-${Date.now().toString(36)}`, + chapter: 1, + reference: '', + title: '', + audioEmbedUrl: '', + passageText: '', + summary: '', + commentary: '', + greekNotes: [], + studyQuestions: [], + }, + ], + } + : study), + })) + } + + function updateStudySection(studyId: string, sectionId: string, field: keyof ColossiansStudySection, value: string | number | string[]) { + setForm(f => ({ + ...f, + studies: (f.studies ?? []).map(study => study.id === studyId + ? { + ...study, + sections: (study.sections ?? []).map(section => section.id === sectionId ? { ...section, [field]: value } : section), + } + : study), + })) + } + + function removeStudySection(studyId: string, sectionId: string) { + setForm(f => ({ + ...f, + studies: (f.studies ?? []).map(study => study.id === studyId + ? { ...study, sections: (study.sections ?? []).filter(section => section.id !== sectionId) } + : study), + })) + } + function archiveCurrentSeriesSnapshot() { const currentTitle = form.seriesTitle.trim() if (!currentTitle) { @@ -1738,6 +1886,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
Content + @@ -2724,6 +2873,156 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { )} + {/* STUDIES */} + {adminView === 'colossians-study' && ( +
+
+

Studies

+

Manage multiple study tracks and their lesson sections (text, commentary, Greek notes, audio, and questions).

+
+ + {(form.studies ?? []).length === 0 &&

No studies yet.

} + + {(form.studies ?? []).map(study => ( +
+ +
+ {study.title || 'Untitled study'} · /study/{study.slug || 'slug'} +

{study.description || 'Study description'}

+
+ Expand to edit +
+
+
+
+
+ + updateStudyProgram(study.id, 'title', e.target.value)} /> +
+
+ + updateStudyProgram(study.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} /> +
+
+ + +
+
+ +