Merge feature/colossians-study-pages

This commit is contained in:
nmemmert
2026-05-12 12:47:22 -04:00
13 changed files with 3701 additions and 18 deletions
+819 -1
View File
@@ -48,6 +48,824 @@
"studyGuideDownloadUrl": "", "studyGuideDownloadUrl": "",
"studyGuideUrl": "https://a.co/d/01sG2tOJ", "studyGuideUrl": "https://a.co/d/01sG2tOJ",
"studyGuideAmazonButtonLabel": "Get it on Amazon", "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": "Pauls 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 Gods initiative.",
"Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.",
"Saints (hagios): Gods 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": "Pauls 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 Gods 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 Christs 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 Christs 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 believers 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": "Pauls Ministry and the Revealed Mystery",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.",
"commentary": "Pauls ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.",
"studyQuestions": [
"What is the goal of Pauls 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": "Pauls 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 believers 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 believers 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 Christs 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 believers 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. Pauls 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 Christs character becomes visible in them.",
"commentary": "The image of clothing makes virtue practical and visible. Paul wants Christs 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 Christs Peace and Word",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul calls the church to let Christs 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 Christs 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 Christs 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 Pauls 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 Pauls 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. Pauls 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 letters 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": "Pauls 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 Gods initiative.",
"Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.",
"Saints (hagios): Gods 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": "Pauls 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 Gods 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 Christs 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 Christs 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 believers former condition?",
"What does continuing in the faith look like?"
]
},
{
"id": "1-24-29",
"chapter": 1,
"reference": "1:24-29",
"title": "Pauls Ministry and the Revealed Mystery",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.",
"commentary": "Pauls 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 Pauls ministry?",
"Why is maturity such an important theme here?"
]
},
{
"id": "2-1-5",
"chapter": 2,
"reference": "2:1-5",
"title": "Pauls 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 believers 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 believers 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 Christs 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 believers 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. Pauls 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 Christs character becomes visible in them.",
"commentary": "The image of clothing makes virtue practical and visible. Paul wants Christs 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 Christs Peace and Word",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul calls the church to let Christs 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 Christs 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 Christs 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 Pauls 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 Pauls 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. Pauls 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 letters themes?"
]
}
],
"shareHeading": "Help one more person hear the Word this week.", "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.", "shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
"whereToNextEyebrow": "Where to Next", "whereToNextEyebrow": "Where to Next",
@@ -183,5 +1001,5 @@
] ]
} }
}, },
"updatedAt": "2026-05-07T15:35:23.888Z" "updatedAt": "2026-05-12T16:46:21.985Z"
} }
+809 -1
View File
@@ -48,6 +48,814 @@
"studyGuideDownloadUrl": "", "studyGuideDownloadUrl": "",
"studyGuideUrl": "https://a.co/d/01sG2tOJ", "studyGuideUrl": "https://a.co/d/01sG2tOJ",
"studyGuideAmazonButtonLabel": "Get it on Amazon", "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": "Pauls 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 Gods initiative.",
"Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.",
"Saints (hagios): Gods 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": "Pauls 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 Gods 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 Christs 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 Christs 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 believers 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": "Pauls Ministry and the Revealed Mystery",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.",
"commentary": "Pauls ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.",
"studyQuestions": [
"What is the goal of Pauls 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": "Pauls 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 believers 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 believers 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 Christs 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 believers 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. Pauls 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 Christs character becomes visible in them.",
"commentary": "The image of clothing makes virtue practical and visible. Paul wants Christs 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 Christs Peace and Word",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul calls the church to let Christs 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 Christs 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 Christs 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 Pauls 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 Pauls 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. Pauls 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 letters 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": "Pauls 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 Gods initiative.",
"Brother (adelphos): a fellow believer; the same spiritual-family language Paul uses throughout his letters.",
"Saints (hagios): Gods 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": "Pauls 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 Gods 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 Christs 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 Christs 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 believers former condition?",
"What does continuing in the faith look like?"
]
},
{
"id": "1-24-29",
"chapter": 1,
"reference": "1:24-29",
"title": "Pauls Ministry and the Revealed Mystery",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul describes his labor to proclaim Christ and present believers mature in Him.",
"commentary": "Pauls 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 Pauls ministry?",
"Why is maturity such an important theme here?"
]
},
{
"id": "2-1-5",
"chapter": 2,
"reference": "2:1-5",
"title": "Pauls 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 believers 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 believers 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 Christs 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 believers 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. Pauls 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 Christs character becomes visible in them.",
"commentary": "The image of clothing makes virtue practical and visible. Paul wants Christs 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 Christs Peace and Word",
"audioEmbedUrl": "",
"passageText": "",
"summary": "Paul calls the church to let Christs 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 Christs 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 Christs 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 Pauls 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 Pauls 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. Pauls 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 letters themes?"
]
}
],
"shareHeading": "Help one more person hear the Word this week.", "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.", "shareP": "Scan the QR code or text the show link to a friend who needs encouragement today.",
"whereToNextEyebrow": "Where to Next", "whereToNextEyebrow": "Where to Next",
@@ -183,5 +991,5 @@
] ]
} }
}, },
"updatedAt": "2026-05-07T15:35:23.896Z" "updatedAt": "2026-05-12T16:06:01.888Z"
} }
+1 -1
View File
@@ -98,5 +98,5 @@
"topic": "Faith & Life" "topic": "Faith & Life"
} }
], ],
"updatedAt": "2026-05-07T15:35:23.896Z" "updatedAt": "2026-05-12T16:06:01.888Z"
} }
+8
View File
@@ -0,0 +1,8 @@
{
"notesByUser": {
"e43e7217-be69-49c6-a65d-edd0e7745884": {
"1-1-2": "Test"
}
},
"updatedAt": "2026-05-12T15:51:54.402Z"
}
+13
View File
@@ -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"
}
+339 -1
View File
@@ -1,7 +1,7 @@
import express from 'express' import express from 'express'
import rateLimit from 'express-rate-limit' import rateLimit from 'express-rate-limit'
import { mkdir, readdir, readFile, stat, unlink, writeFile } from 'node:fs/promises' 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 path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { Resend } from 'resend' 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 CONTACT_SUBMISSIONS_FILE = path.join(DATA_DIR, 'contact-submissions.json')
const QUESTIONS_FILE = path.join(DATA_DIR, 'questions.json') const QUESTIONS_FILE = path.join(DATA_DIR, 'questions.json')
const DRAFT_QUESTIONS_FILE = path.join(DATA_DIR, 'questions-draft.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_TEMPLATES_FILE = path.join(DATA_DIR, 'admin-reply-templates.json')
const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json') const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json')
const BACKUP_DIR = path.join(DATA_DIR, 'backups') 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 titusDownloadTokens = new Map()
const MAX_QUESTIONS = 1000 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 visitorStats = { ...EMPTY_VISITOR_STATS }
let visitorStatsWritePromise = Promise.resolve() let visitorStatsWritePromise = Promise.resolve()
let contactSubmissions = [] let contactSubmissions = []
let contactSubmissionsWritePromise = Promise.resolve() let contactSubmissionsWritePromise = Promise.resolve()
let questions = [] let questions = []
let questionsWritePromise = Promise.resolve() let questionsWritePromise = Promise.resolve()
let studyUsers = []
let studyUsersWritePromise = Promise.resolve()
let studyNotesByUser = {}
let studyNotesWritePromise = Promise.resolve()
let downloadCounts = {} let downloadCounts = {}
let downloadCountsWritePromise = Promise.resolve() let downloadCountsWritePromise = Promise.resolve()
let lastVisitorStatsWrite = { ok: true, at: null, error: null } 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 lastBackupStatus = { ok: true, at: null, error: null, file: null }
let lastCachePurgeStatus = { ok: true, at: null, error: null } let lastCachePurgeStatus = { ok: true, at: null, error: null }
let lastDeployHookStatus = { ok: true, at: null, error: null } let lastDeployHookStatus = { ok: true, at: null, error: null }
const studySessions = new Map()
function sanitizeReplyTemplates(value) { function sanitizeReplyTemplates(value) {
if (!Array.isArray(value)) return [...DEFAULT_REPLY_TEMPLATES] if (!Array.isArray(value)) return [...DEFAULT_REPLY_TEMPLATES]
@@ -1543,6 +1555,190 @@ function loadQuestionsFromDisk() {
questions = [] 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 // Rate limiter: max 10 attempts per 15 minutes per IP on the login endpoint
const loginRateLimiter = rateLimit({ const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
@@ -1553,6 +1749,146 @@ const loginRateLimiter = rateLimit({
skipSuccessfulRequests: true, 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) => { app.get('/api/admin-auth/status', async (req, res) => {
res.json({ res.json({
authenticated: isValidAdminSession(req), authenticated: isValidAdminSession(req),
@@ -3058,6 +3394,8 @@ Promise.all([
loadReplyHistoryFromDisk(), loadReplyHistoryFromDisk(),
loadQuestionsFromDisk(), loadQuestionsFromDisk(),
loadDraftQuestionsFromDisk(), loadDraftQuestionsFromDisk(),
loadStudyUsersFromDisk(),
loadStudyNotesFromDisk(),
loadDownloadCountsFromDisk(), loadDownloadCountsFromDisk(),
refreshContentCaches(), refreshContentCaches(),
]) ])
+49
View File
@@ -225,6 +225,53 @@ function sanitizeArchivedSeriesNotes(value) {
.filter(item => item.heading || item.body) .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) { function sanitizeArchivedSeries(value) {
const source = Array.isArray(value) ? value : [] const source = Array.isArray(value) ? value : []
return source return source
@@ -269,6 +316,8 @@ export function sanitizeSiteContent(siteContent) {
customLinks: sanitizeCustomLinks(siteContent.customLinks), customLinks: sanitizeCustomLinks(siteContent.customLinks),
customBlocks: sanitizeCustomBlocks(siteContent.customBlocks), customBlocks: sanitizeCustomBlocks(siteContent.customBlocks),
archivedSeries: sanitizeArchivedSeries(siteContent.archivedSeries), archivedSeries: sanitizeArchivedSeries(siteContent.archivedSeries),
studies: sanitizeStudies(siteContent.studies),
colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections),
redirects: sanitizeRedirectRules(siteContent.redirects), redirects: sanitizeRedirectRules(siteContent.redirects),
podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS), podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS),
seo: { seo: {
+311 -12
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import type { ChangeEvent } from 'react' import type { ChangeEvent } from 'react'
import { Link } from 'react-router-dom' 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 { DEFAULTS } from './content'
import { AnalyticsPanel } from './components/AnalyticsPanel' import { AnalyticsPanel } from './components/AnalyticsPanel'
@@ -156,13 +156,13 @@ interface ContactReplyConfig {
note: string note: string
} }
type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'archivedSeries' | 'redirects' | 'podcastFeaturedLinks' | 'seo' | 'legal' | 'whereToNextCards'> type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'archivedSeries' | 'redirects' | 'podcastFeaturedLinks' | 'seo' | 'legal' | 'whereToNextCards' | 'colossiansStudySections' | 'studies'>
type AdminView = type AdminView =
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact' | 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
| 'current-series' | 'episode-highlights' | 'archived-series' | 'current-series' | 'episode-highlights' | 'archived-series'
| 'downloads' | 'custom-links' | 'content-blocks' | 'downloads' | 'custom-links' | 'content-blocks'
| 'questions' | 'analytics' | 'assets' | 'questions' | 'analytics' | 'assets' | 'colossians-study'
| 'emails' | 'subscribers' | 'contacts' | 'emails' | 'subscribers' | 'contacts'
| 'seo' | 'legal' | 'security' | 'brand' | 'global' | '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' }, { 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) { export default function AdminPage({ content, onSave, onLogout }: Props) {
const [form, setForm] = useState<SiteContent>(content) const normalizedContent = normalizeSiteContentForAdmin(content)
const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(content)) const [form, setForm] = useState<SiteContent>(normalizedContent)
const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(normalizedContent))
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('') const [errorMsg, setErrorMsg] = useState('')
const [adminView, setAdminView] = useState<AdminView>('dashboard') const [adminView, setAdminView] = useState<AdminView>('dashboard')
@@ -368,8 +427,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
}, [form, adminView, previewOpen]) }, [form, adminView, previewOpen])
useEffect(() => { useEffect(() => {
const nextSnapshot = JSON.stringify(content) const normalized = normalizeSiteContentForAdmin(content)
setForm(content) const nextSnapshot = JSON.stringify(normalized)
setForm(normalized)
setLastSavedSnapshot(nextSnapshot) setLastSavedSnapshot(nextSnapshot)
}, [content]) }, [content])
@@ -575,7 +635,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
if (!r.ok) return if (!r.ok) return
const data = await r.json() as { siteContent?: Partial<SiteContent> } const data = await r.json() as { siteContent?: Partial<SiteContent> }
if (data?.siteContent && typeof data.siteContent === 'object') { if (data?.siteContent && typeof data.siteContent === 'object') {
const next = { ...DEFAULTS, ...data.siteContent } const next = normalizeSiteContentForAdmin({ ...DEFAULTS, ...data.siteContent })
setForm(next) setForm(next)
onSave(next) onSave(next)
} }
@@ -601,13 +661,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
} }
async function handleSaveDraft() { async function handleSaveDraft() {
const payload = buildSiteContentForSave(form)
setStatus('saving') setStatus('saving')
setErrorMsg('') setErrorMsg('')
try { try {
const res = await fetch('/api/admin-content-draft', { const res = await fetch('/api/admin-content-draft', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ siteContent: form }), body: JSON.stringify({ siteContent: payload }),
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json().catch(() => ({})) 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 } const data = await res.json() as { updatedAt?: string }
setPublishState(prev => ({ ...prev, draftUpdatedAt: data.updatedAt ?? new Date().toISOString() })) setPublishState(prev => ({ ...prev, draftUpdatedAt: data.updatedAt ?? new Date().toISOString() }))
setLastSavedSnapshot(JSON.stringify(form)) setLastSavedSnapshot(JSON.stringify(payload))
setStatus('saved') setStatus('saved')
setTimeout(() => setStatus('idle'), 3500) setTimeout(() => setStatus('idle'), 3500)
} catch (err) { } catch (err) {
@@ -626,6 +687,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
async function handlePublishDraft() { async function handlePublishDraft() {
if (!confirm('Publish current changes to the live site now?')) return if (!confirm('Publish current changes to the live site now?')) return
const payload = buildSiteContentForSave(form)
setStatus('saving') setStatus('saving')
setErrorMsg('') setErrorMsg('')
try { try {
@@ -633,7 +695,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const draftRes = await fetch('/api/admin-content-draft', { const draftRes = await fetch('/api/admin-content-draft', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ siteContent: form }), body: JSON.stringify({ siteContent: payload }),
}) })
if (!draftRes.ok) { if (!draftRes.ok) {
const data = await draftRes.json().catch(() => ({})) 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') if (!latestRes.ok) throw new Error('Failed to refresh published content')
const latest = await latestRes.json() as { siteContent?: Partial<SiteContent> } const latest = await latestRes.json() as { siteContent?: Partial<SiteContent> }
if (latest?.siteContent) { if (latest?.siteContent) {
const next = { ...DEFAULTS, ...latest.siteContent } const next = normalizeSiteContentForAdmin({ ...DEFAULTS, ...latest.siteContent })
setForm(next) setForm(next)
onSave(next) onSave(next)
setLastSavedSnapshot(JSON.stringify(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<StudyProgram, 'sections'>, 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() { function archiveCurrentSeriesSnapshot() {
const currentTitle = form.seriesTitle.trim() const currentTitle = form.seriesTitle.trim()
if (!currentTitle) { if (!currentTitle) {
@@ -1738,6 +1886,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<div className="admin-nav-group"> <div className="admin-nav-group">
<span className="admin-nav-label">Content</span> <span className="admin-nav-label">Content</span>
<button type="button" className={`admin-nav-item${adminView === 'colossians-study' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('colossians-study')}>Studies</button>
<button type="button" className={`admin-nav-item${adminView === 'downloads' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('downloads')}>Downloads</button> <button type="button" className={`admin-nav-item${adminView === 'downloads' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('downloads')}>Downloads</button>
<button type="button" className={`admin-nav-item${adminView === 'custom-links' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('custom-links')}>Custom Links</button> <button type="button" className={`admin-nav-item${adminView === 'custom-links' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('custom-links')}>Custom Links</button>
<button type="button" className={`admin-nav-item${adminView === 'content-blocks' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('content-blocks')}>Content Blocks</button> <button type="button" className={`admin-nav-item${adminView === 'content-blocks' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('content-blocks')}>Content Blocks</button>
@@ -2724,6 +2873,156 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</section> </section>
)} )}
{/* STUDIES */}
{adminView === 'colossians-study' && (
<section className="admin-panel-section">
<div className="admin-panel-head">
<h2>Studies</h2>
<p>Manage multiple study tracks and their lesson sections (text, commentary, Greek notes, audio, and questions).</p>
</div>
{(form.studies ?? []).length === 0 && <p className="admin-stats-note">No studies yet.</p>}
{(form.studies ?? []).map(study => (
<details key={study.id} className="admin-collapsible-card" open={false}>
<summary className="admin-collapsible-summary">
<div>
<strong>{study.title || 'Untitled study'} · /study/{study.slug || 'slug'}</strong>
<p>{study.description || 'Study description'}</p>
</div>
<span className="admin-collapsible-hint">Expand to edit</span>
</summary>
<div className="admin-collapsible-body">
<div className="admin-array-row" style={{ marginBottom: '1rem' }}>
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`study-title-${study.id}`}>Study Title</label>
<input id={`study-title-${study.id}`} type="text" value={study.title} placeholder="Colossians: Rooted in Christ" onChange={e => updateStudyProgram(study.id, 'title', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-slug-${study.id}`}>Slug</label>
<input id={`study-slug-${study.id}`} type="text" value={study.slug} placeholder="colossians" onChange={e => updateStudyProgram(study.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} />
</div>
<div className="admin-field">
<label htmlFor={`study-status-${study.id}`}>Status</label>
<select id={`study-status-${study.id}`} value={study.status} onChange={e => updateStudyProgram(study.id, 'status', e.target.value)}>
<option value="active">Active</option>
<option value="planned">Planned</option>
</select>
</div>
<div className="admin-field">
<label htmlFor={`study-description-${study.id}`}>Description</label>
<textarea id={`study-description-${study.id}`} rows={3} value={study.description} placeholder="Brief description for the study hub card" onChange={e => updateStudyProgram(study.id, 'description', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-home-eyebrow-${study.id}`}>Homepage Card Eyebrow</label>
<input id={`study-home-eyebrow-${study.id}`} type="text" value={study.homepageEyebrow ?? ''} placeholder="New Study" onChange={e => updateStudyProgram(study.id, 'homepageEyebrow', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-new-tag-label-${study.id}`}>NEW Tag Label</label>
<input id={`study-new-tag-label-${study.id}`} type="text" value={study.newTagLabel ?? 'NEW'} placeholder="NEW" onChange={e => updateStudyProgram(study.id, 'newTagLabel', e.target.value)} />
</div>
<div className="admin-field" style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<input id={`study-home-feature-${study.id}`} type="checkbox" checked={study.showOnHomepage === true} onChange={e => updateStudyProgram(study.id, 'showOnHomepage', e.target.checked)} />
<label htmlFor={`study-home-feature-${study.id}`}>Feature this study card on homepage</label>
</div>
<div className="admin-field" style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<input id={`study-show-new-tag-${study.id}`} type="checkbox" checked={study.showNewTag === true} onChange={e => updateStudyProgram(study.id, 'showNewTag', e.target.checked)} />
<label htmlFor={`study-show-new-tag-${study.id}`}>Show NEW tag on homepage card</label>
</div>
</div>
<div className="admin-field">
<label htmlFor={`study-chapters-${study.id}`}>Number of Chapters</label>
<input id={`study-chapters-${study.id}`} type="number" min="1" max="999" value={study.numberOfChapters} placeholder="4" onChange={e => updateStudyProgram(study.id, "numberOfChapters", Number(e.target.value) || 1)} />
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeStudyProgram(study.id)}>Remove Study</button>
</div>
{(study.sections ?? []).length === 0 && (
<p className="admin-stats-note">No sections yet for this study.</p>
)}
{(study.sections ?? []).map(section => (
<details key={section.id} className="admin-collapsible-card" open={false}>
<summary className="admin-collapsible-summary">
<div>
<strong>Lesson {section.reference || section.id}</strong>
<p>{section.title || 'Untitled section'}</p>
</div>
<span className="admin-collapsible-hint">Expand to edit lesson</span>
</summary>
<div className="admin-collapsible-body">
<div className="admin-array-row">
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`study-section-chapter-${study.id}-${section.id}`}>Chapter</label>
<select id={`study-section-chapter-${study.id}-${section.id}`} value={section.chapter} onChange={e => updateStudySection(study.id, section.id, 'chapter', Number(e.target.value) || 1)}>
{Array.from({ length: Math.max(study.numberOfChapters, section.chapter, 1) }, (_, index) => index + 1).map(chapterNumber => (
<option key={chapterNumber} value={chapterNumber}>Chapter {chapterNumber}</option>
))}
</select>
</div>
<div className="admin-field">
<label htmlFor={`study-section-reference-${study.id}-${section.id}`}>Passage Reference</label>
<input id={`study-section-reference-${study.id}-${section.id}`} type="text" value={section.reference} placeholder="1:1-2" onChange={e => updateStudySection(study.id, section.id, 'reference', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-title-${study.id}-${section.id}`}>Section Title</label>
<input id={`study-section-title-${study.id}-${section.id}`} type="text" value={section.title} placeholder="Paul's Greeting" onChange={e => updateStudySection(study.id, section.id, 'title', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-audio-${study.id}-${section.id}`}>Spotify Embed URL (optional)</label>
<input id={`study-section-audio-${study.id}-${section.id}`} type="url" value={section.audioEmbedUrl ?? ''} placeholder="https://open.spotify.com/embed/episode/..." onChange={e => updateStudySection(study.id, section.id, 'audioEmbedUrl', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-released-${study.id}-${section.id}`}>Release Date (optional)</label>
<input id={`study-section-released-${study.id}-${section.id}`} type="datetime-local" value={section.releasedAt ? new Date(section.releasedAt).toISOString().slice(0, 16) : ''} onChange={e => {
const val = e.target.value;
if (val) {
const date = new Date(val + ':00Z');
updateStudySection(study.id, section.id, 'releasedAt', date.toISOString());
} else {
updateStudySection(study.id, section.id, 'releasedAt', '');
}
}} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-summary-${study.id}-${section.id}`}>Short Summary</label>
<textarea id={`study-section-summary-${study.id}-${section.id}`} rows={3} value={section.summary} placeholder="One to two sentences that summarize the section." onChange={e => updateStudySection(study.id, section.id, 'summary', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-passage-${study.id}-${section.id}`}>Passage Text</label>
<textarea id={`study-section-passage-${study.id}-${section.id}`} rows={4} value={section.passageText} placeholder="Paste the passage text here." onChange={e => updateStudySection(study.id, section.id, 'passageText', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-commentary-${study.id}-${section.id}`}>Commentary</label>
<textarea id={`study-section-commentary-${study.id}-${section.id}`} rows={5} value={section.commentary} placeholder="Add your teaching notes and explanation here." onChange={e => updateStudySection(study.id, section.id, 'commentary', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-greek-${study.id}-${section.id}`}>Greek Words</label>
<textarea id={`study-section-greek-${study.id}-${section.id}`} rows={4} value={(section.greekNotes ?? []).join('\n')} placeholder="One note per line" onChange={e => updateStudySection(study.id, section.id, 'greekNotes', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-questions-${study.id}-${section.id}`}>Study Questions</label>
<textarea id={`study-section-questions-${study.id}-${section.id}`} rows={5} value={(section.studyQuestions ?? []).join('\n')} placeholder="One question per line" onChange={e => updateStudySection(study.id, section.id, 'studyQuestions', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeStudySection(study.id, section.id)}>Remove Lesson</button>
</div>
</div>
</details>
))}
<button type="button" className="btn-admin-add" onClick={() => addStudySection(study.id)}>+ Add Lesson Section</button>
</div>
</details>
))}
<button type="button" className="btn-admin-add" onClick={addStudyProgram}>+ Add Study Program</button>
{renderSaveStatus()}
</section>
)}
{/* GLOBAL / FOOTER & PLATFORM */} {/* GLOBAL / FOOTER & PLATFORM */}
{adminView === 'global' && ( {adminView === 'global' && (
<section className="admin-panel-section"> <section className="admin-panel-section">
+485
View File
@@ -267,6 +267,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
background: transparent;
color: var(--brand-warm-white); color: var(--brand-warm-white);
font-family: var(--brand-font-body); font-family: var(--brand-font-body);
font-weight: 500; font-weight: 500;
@@ -965,6 +966,490 @@
font-size: 0.97rem; font-size: 0.97rem;
} }
.home-jump-card--featured {
border-color: rgba(201, 168, 76, 0.38);
background: linear-gradient(180deg, rgba(201, 168, 76, 0.1), rgba(12, 12, 11, 0.96));
}
.home-jump-card--featured:hover {
border-color: rgba(201, 168, 76, 0.55);
background: linear-gradient(180deg, rgba(201, 168, 76, 0.16), rgba(14, 14, 12, 0.98));
}
.home-jump-featured-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.45rem;
}
.home-jump-new-tag {
display: inline-block;
padding: 0.12rem 0.48rem;
border-radius: 999px;
border: 1px solid rgba(201, 168, 76, 0.7);
background: rgba(201, 168, 76, 0.22);
color: #f4e3b0;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1.2;
}
/* ── Colossians Study ── */
.study-index-page,
.study-section-page {
background:
radial-gradient(1200px 500px at 90% -10%, rgba(201, 168, 76, 0.12), transparent 55%),
radial-gradient(900px 450px at 0% 0%, rgba(255, 255, 255, 0.05), transparent 55%),
#090909;
}
.section-study-course-hero {
padding: 4.5rem 0 1.5rem;
}
.study-course-hero-inner {
max-width: 860px;
}
.study-course-hero-copy {
max-width: 66ch;
margin: 0.85rem 0 1.5rem;
color: var(--brand-muted);
}
.study-course-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.25rem;
}
.study-course-meta span {
padding: 0.4rem 0.65rem;
border: 1px solid rgba(201, 168, 76, 0.28);
border-radius: 999px;
color: var(--brand-warm-white);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.study-course-hero-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.section-study-hub-flow {
padding: 0.5rem 0 0.25rem;
}
.study-hub-flow-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.8rem;
}
.study-hub-flow-card {
padding: 1rem 1rem;
border-radius: 12px;
border: 1px solid rgba(201, 168, 76, 0.16);
background: rgba(16, 16, 14, 0.86);
}
.study-hub-flow-card h3 {
margin: 0.25rem 0 0.4rem;
font-family: var(--brand-font-heading);
color: var(--brand-warm-white);
}
.study-hub-flow-card p:last-child {
margin: 0;
color: var(--brand-muted);
line-height: 1.5;
}
.section-study-syllabus {
padding: 0.5rem 0 0.25rem;
}
.study-syllabus-header {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1.5rem;
align-items: flex-end;
justify-content: space-between;
}
.study-syllabus-header h2 {
margin: 0;
font-family: var(--brand-font-heading);
color: var(--brand-warm-white);
}
.study-syllabus-header p {
margin: 0;
color: var(--brand-muted);
max-width: 70ch;
}
.section-study-module {
padding: 1rem 0 0;
}
.study-module-header {
margin-bottom: 0.85rem;
}
.study-module-header p {
margin: 0;
color: var(--brand-gold);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.72rem;
}
.study-module-header h2 {
margin: 0.35rem 0 0;
font-family: var(--brand-font-heading);
font-size: clamp(1.32rem, 2.1vw, 1.85rem);
color: var(--brand-warm-white);
}
.study-module-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.study-module-row {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 1rem;
padding: 1rem 1.05rem;
border-radius: 12px;
border: 1px solid rgba(201, 168, 76, 0.14);
background: rgba(17, 17, 15, 0.84);
color: inherit;
text-decoration: none;
}
.study-module-row:hover {
border-color: rgba(201, 168, 76, 0.35);
background: rgba(28, 27, 23, 0.92);
}
.study-module-row--disabled,
.study-module-row--disabled:hover {
border-style: dashed;
border-color: rgba(201, 168, 76, 0.22);
background: rgba(13, 13, 11, 0.78);
cursor: default;
}
.study-module-row-left h3 {
margin: 0.25rem 0 0.4rem;
font-family: var(--brand-font-heading);
color: var(--brand-warm-white);
}
.study-module-lesson {
margin: 0;
color: var(--brand-gold);
text-transform: uppercase;
letter-spacing: 0.11em;
font-size: 0.73rem;
}
.study-module-row-left p:last-child,
.study-module-row-right p:last-child {
margin: 0;
color: var(--brand-muted);
line-height: 1.5;
}
.study-module-row-right {
border-left: 1px solid rgba(201, 168, 76, 0.18);
padding-left: 0.9rem;
}
.study-section-reference {
margin: 0 0 0.2rem;
color: var(--brand-gold);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.74rem;
}
.study-module-focus {
margin: 0 0 0.2rem;
color: var(--brand-warm-white);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.section-study-classroom {
padding: 3.75rem 0 1.75rem;
}
.study-classroom-shell {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr);
gap: 1rem;
align-items: start;
}
.study-detail-back {
display: inline-block;
margin-bottom: 0.9rem;
color: var(--brand-gold);
text-decoration: none;
}
.study-lesson-label {
margin: 0;
color: var(--brand-gold);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.73rem;
}
.study-classroom-main h1 {
margin: 0.35rem 0 0;
}
.study-detail-reference {
margin: 0.55rem 0 0;
color: var(--brand-gold);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.78rem;
}
.study-detail-summary {
max-width: 70ch;
margin: 0.85rem 0 0;
color: var(--brand-muted);
}
.study-class-block {
margin-top: 1rem;
padding: 1rem 1rem;
border-radius: 12px;
border: 1px solid rgba(201, 168, 76, 0.14);
background: rgba(18, 18, 16, 0.86);
}
.study-class-block h2 {
margin: 0 0 0.7rem;
font-family: var(--brand-font-heading);
color: var(--brand-warm-white);
}
.study-audio-embed-wrap {
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(201, 168, 76, 0.2);
}
.study-detail-copy--scripture {
white-space: pre-line;
}
.study-detail-copy,
.study-detail-list {
margin: 0;
color: var(--brand-muted);
}
.study-detail-list {
padding-left: 1.2rem;
}
.study-detail-list li + li {
margin-top: 0.5rem;
}
.study-classroom-sidebar {
position: sticky;
top: 5.75rem;
}
.study-class-side-block {
margin-bottom: 0.85rem;
padding: 1rem 1rem;
border-radius: 12px;
border: 1px solid rgba(201, 168, 76, 0.14);
background: rgba(18, 18, 16, 0.86);
}
.study-class-side-block h3 {
margin: 0 0 0.7rem;
font-family: var(--brand-font-heading);
color: var(--brand-warm-white);
}
.study-class-side-block p {
margin: 0;
color: var(--brand-muted);
}
.study-class-side-block p + p {
margin-top: 0.45rem;
}
.study-auth-box,
.study-notes-box {
display: grid;
gap: 0.5rem;
}
.study-auth-box label {
font-size: 0.8rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--brand-gold);
}
.study-auth-box input,
.study-notes-box textarea {
width: 100%;
border-radius: 10px;
border: 1px solid rgba(201, 168, 76, 0.26);
background: rgba(10, 10, 9, 0.9);
color: var(--brand-warm-white);
font-family: var(--brand-font-body);
font-size: 0.98rem;
line-height: 1.5;
padding: 0.6rem 0.65rem;
}
.study-auth-box input:focus,
.study-notes-box textarea:focus {
outline: 1px solid rgba(201, 168, 76, 0.65);
border-color: rgba(201, 168, 76, 0.5);
}
.study-auth-actions {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
}
.study-auth-actions .btn-primary,
.study-auth-actions .btn-secondary {
min-width: 126px;
}
.study-auth-actions .btn-secondary {
background: rgba(15, 15, 13, 0.86);
}
.study-notes-user {
color: var(--brand-gold) !important;
font-size: 0.86rem;
letter-spacing: 0.04em;
margin: 0 !important;
}
.study-note-status {
margin-top: 0.65rem;
color: var(--brand-muted);
font-size: 0.92rem;
}
.study-detail-nav {
display: flex;
align-items: stretch;
justify-content: space-between;
gap: 1rem;
margin-top: 0.25rem;
}
.study-detail-nav .btn-secondary {
width: 100%;
text-align: center;
}
.study-nav-placeholder {
flex: 1 1 auto;
}
.section-study-homework {
padding: 0.2rem 0 4rem;
}
.study-homework-card {
padding: 1rem 1rem;
border-radius: 12px;
border: 1px solid rgba(201, 168, 76, 0.2);
background: linear-gradient(180deg, rgba(201, 168, 76, 0.12), rgba(20, 20, 18, 0.92));
}
.study-homework-card h2 {
margin: 0 0 0.65rem;
font-family: var(--brand-font-heading);
color: var(--brand-warm-white);
}
.study-homework-card p {
margin: 0;
color: var(--brand-muted);
}
.study-homework-note {
margin-top: 0.6rem !important;
font-size: 0.92rem;
color: var(--brand-gold) !important;
}
@media (max-width: 980px) {
.study-classroom-shell {
grid-template-columns: 1fr;
}
.study-classroom-sidebar {
position: static;
}
}
@media (max-width: 840px) {
.study-hub-flow-grid {
grid-template-columns: 1fr;
}
.study-module-row {
grid-template-columns: 1fr;
}
.study-module-row-right {
border-left: none;
border-top: 1px solid rgba(201, 168, 76, 0.18);
padding-left: 0;
padding-top: 0.7rem;
}
.study-detail-nav {
flex-direction: column;
}
.study-auth-actions {
display: grid;
grid-template-columns: 1fr;
}
.study-auth-actions .btn-primary,
.study-auth-actions .btn-secondary {
width: 100%;
justify-content: center;
}
}
/* ── Episode Detail Page ── */ /* ── Episode Detail Page ── */
.section-episode-detail { .section-episode-detail {
background: var(--brand-black); background: var(--brand-black);
+27 -1
View File
@@ -3,8 +3,9 @@ import { Link, NavLink, Routes, Route, useLocation, useNavigate, useParams } fro
import AdminPage from './AdminPage' import AdminPage from './AdminPage'
import QASection from './components/QASection' import QASection from './components/QASection'
import ContactForm from './components/ContactForm' import ContactForm from './components/ContactForm'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage } from './colossiansStudy'
import { SpotifyIcon } from './icons' import { SpotifyIcon } from './icons'
import type { SiteContent, ArchivedSeries } from './content' import type { SiteContent, ArchivedSeries, StudyProgram } from './content'
import { DEFAULTS } from './content' import { DEFAULTS } from './content'
import './App.css' import './App.css'
@@ -55,6 +56,14 @@ function isLikelySpotifyEpisodeUrl(url: string | undefined): boolean {
return false return false
} }
function getHomepageFeaturedStudy(content: SiteContent): StudyProgram | null {
const studies = Array.isArray(content.studies) ? content.studies : []
if (studies.length === 0) return null
return studies.find(study => study.showOnHomepage === true)
?? studies.find(study => study.status === 'active')
?? studies[0]
}
function HeadlinerWidget() { function HeadlinerWidget() {
const [iframeSrc, setIframeSrc] = useState('') const [iframeSrc, setIframeSrc] = useState('')
const [status, setStatus] = useState<'loading' | 'ready' | 'empty'>('loading') const [status, setStatus] = useState<'loading' | 'ready' | 'empty'>('loading')
@@ -457,6 +466,7 @@ function SiteHeader({ content }: { content: SiteContent }) {
<nav id="site-nav" className={`header-nav ${menuOpen ? 'header-nav--open' : ''}`}> <nav id="site-nav" className={`header-nav ${menuOpen ? 'header-nav--open' : ''}`}>
<NavLink to="/episodes" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Episodes</NavLink> <NavLink to="/episodes" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Episodes</NavLink>
<NavLink to="/questions" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Q&amp;A</NavLink> <NavLink to="/questions" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Q&amp;A</NavLink>
<NavLink to="/study" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Studies</NavLink>
<NavLink to="/resources" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Downloads</NavLink> <NavLink to="/resources" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Downloads</NavLink>
<NavLink to="/about" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>About</NavLink> <NavLink to="/about" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>About</NavLink>
<NavLink to="/contact" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Contact</NavLink> <NavLink to="/contact" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Contact</NavLink>
@@ -939,6 +949,8 @@ function CustomBlocksSection({ content, page }: { content: SiteContent; page: st
} }
function LandingPage({ content }: { content: SiteContent }) { function LandingPage({ content }: { content: SiteContent }) {
const featuredStudy = getHomepageFeaturedStudy(content)
return ( return (
<div className="site"> <div className="site">
<SiteHeader content={content} /> <SiteHeader content={content} />
@@ -1024,6 +1036,16 @@ function LandingPage({ content }: { content: SiteContent }) {
<p>{card.description}</p> <p>{card.description}</p>
</Link> </Link>
))} ))}
{featuredStudy && (
<Link to={`/study/${featuredStudy.slug || ''}`} className="home-jump-card home-jump-card--featured">
<div className="home-jump-featured-head">
<p className="eyebrow">{featuredStudy.homepageEyebrow || 'Study'}</p>
{featuredStudy.showNewTag && <span className="home-jump-new-tag">{featuredStudy.newTagLabel || 'NEW'}</span>}
</div>
<h3>{featuredStudy.title}</h3>
<p>{featuredStudy.description || 'Explore this study track with lesson notes, commentary, and guided questions.'}</p>
</Link>
)}
</div> </div>
</div> </div>
</section> </section>
@@ -1908,6 +1930,10 @@ export default function App() {
<Routes> <Routes>
<Route path="/" element={<LandingPage content={content} />} /> <Route path="/" element={<LandingPage content={content} />} />
<Route path="/start-here" element={<StartHerePage content={content} />} /> <Route path="/start-here" element={<StartHerePage content={content} />} />
<Route path="/study" element={<StudyLandingPage content={content} />} />
<Route path="/study/:studySlug" element={<ColossiansStudyIndexPage content={content} />} />
<Route path="/study/:studySlug/notes" element={<ColossiansStudyNotesPage content={content} />} />
<Route path="/study/:studySlug/:sectionId" element={<ColossiansStudySectionPage content={content} />} />
<Route path="/episodes" element={<EpisodesPage content={content} />} /> <Route path="/episodes" element={<EpisodesPage content={content} />} />
<Route path="/episodes/:id" element={<EpisodeDetailPage content={content} />} /> <Route path="/episodes/:id" element={<EpisodeDetailPage content={content} />} />
<Route path="/resources" element={<ResourcesPage content={content} />} /> <Route path="/resources" element={<ResourcesPage content={content} />} />
+697
View File
@@ -0,0 +1,697 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import type { SiteContent, StudyProgram, StudySection } from './content'
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './colossiansStudyData'
type Props = { content: SiteContent }
type StudyAuthState = {
checked: boolean
authenticated: boolean
username: string
}
type StudyNotesMap = Record<string, string>
function getLegacyColossiansStudy(content: SiteContent): StudyProgram {
const legacySections = content.colossiansStudySections?.length ? content.colossiansStudySections : DEFAULT_COLOSSIANS_STUDY_SECTIONS
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.',
status: 'active',
difficulty: 'intermediate',
estimatedHours: 12,
completionBadge: 'Colossians Completion',
numberOfChapters: 4,
sections: legacySections,
}
}
function getStudies(content: SiteContent): StudyProgram[] {
if (Array.isArray(content.studies) && content.studies.length > 0) return content.studies
return [getLegacyColossiansStudy(content)]
}
function getStudyBySlug(studies: StudyProgram[], slug: string | undefined) {
const safeSlug = (slug ?? 'colossians').trim().toLowerCase()
return studies.find(study => study.slug === safeSlug)
}
function getSectionById(sections: StudySection[], sectionId: string | undefined) {
if (!sectionId) return undefined
return sections.find(section => section.id === sectionId)
}
function getLessonNumber(sections: StudySection[], sectionId: string | undefined) {
const index = sections.findIndex(item => item.id === sectionId)
return index >= 0 ? index + 1 : 0
}
function getPrimaryQuestion(questions: string[]) {
return questions.length > 0 ? questions[0] : 'How does this passage shape the way we follow Christ this week?'
}
function getChapterSummaries(study: StudyProgram) {
const highestSectionChapter = study.sections.reduce((max, section) => Math.max(max, section.chapter), 0)
const totalChapters = Math.max(study.numberOfChapters, highestSectionChapter)
return Array.from({ length: totalChapters }, (_, index) => {
const chapter = index + 1
const lessonCount = study.sections.filter(section => section.chapter === chapter).length
return { chapter, lessonCount }
})
}
function isNewLesson(section: StudySection): boolean {
if (!section.releasedAt) return false
const releaseDate = new Date(section.releasedAt)
const now = new Date()
const daysSinceRelease = (now.getTime() - releaseDate.getTime()) / (1000 * 60 * 60 * 24)
return daysSinceRelease >= 0 && daysSinceRelease <= 14
}
function isComingSoon(section: StudySection): boolean {
if (!section.releasedAt) return false
const releaseDate = new Date(section.releasedAt)
const now = new Date()
return releaseDate > now
}
function getReleaseDateDisplay(section: StudySection): string {
if (!section.releasedAt) return ''
const date = new Date(section.releasedAt)
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
function getNoteId(studySlug: string, sectionId: string) {
return `${studySlug}--${sectionId}`
}
async function readJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init)
const data = await response.json().catch(() => ({})) as T & { message?: string }
if (!response.ok) {
throw new Error((data as { message?: string }).message ?? 'Request failed')
}
return data as T
}
export function StudyLandingPage({ content }: Props) {
const studies = getStudies(content)
const activeStudies = studies.filter(study => study.status !== 'planned')
const plannedStudies = studies.filter(study => study.status === 'planned')
const firstActiveStudy = activeStudies[0]
return (
<main className="study-index-page" aria-label="Study hub">
<section className="section-study-course-hero">
<div className="section-inner study-course-hero-inner">
<p className="eyebrow">Self-Paced Bible Academy</p>
<h1>Study at Your Own Pace</h1>
<p className="study-course-hero-copy">Pick a study track, move lesson by lesson on your own schedule, and keep personal notes as you grow through each passage.</p>
<div className="study-course-meta">
<span>{studies.length} study tracks</span>
<span>Self-paced flow</span>
<span>Personal notes</span>
<span>Audio + commentary</span>
</div>
<div className="study-course-hero-actions">
{firstActiveStudy && <Link to={`/study/${firstActiveStudy.slug}`} className="btn-primary">Start Learning</Link>}
<Link to="#study-tracks" className="btn-secondary">Browse Tracks</Link>
</div>
</div>
</section>
<section className="section-study-hub-flow" aria-label="How self-paced study works">
<div className="section-inner">
<div className="study-module-header">
<p>How It Works</p>
<h2>A Simple Self-Paced Rhythm</h2>
</div>
<div className="study-hub-flow-grid">
<article className="study-hub-flow-card">
<p className="study-module-lesson">Step 1</p>
<h3>Choose Your Track</h3>
<p>Start with any active study and begin at lesson one, or jump back in where you left off.</p>
</article>
<article className="study-hub-flow-card">
<p className="study-module-lesson">Step 2</p>
<h3>Work Each Lesson</h3>
<p>Read the text, listen to audio, review commentary, and process key Greek word notes.</p>
</article>
<article className="study-hub-flow-card">
<p className="study-module-lesson">Step 3</p>
<h3>Save Notes and Continue</h3>
<p>Keep personal notes per lesson and build your own study archive over time.</p>
</article>
</div>
</div>
</section>
<section id="study-tracks" className="section-study-module" aria-label="Available studies">
<div className="section-inner">
<div className="study-module-header">
<p>Available Now</p>
<h2>Current Study Tracks</h2>
</div>
<div className="study-module-list">
{activeStudies.map(study => (
<Link key={study.id} to={`/study/${study.slug}`} className="study-module-row">
<div className="study-module-row-left">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
<p className="study-module-lesson">Start Anytime</p>
{study.difficulty && <span style={{ backgroundColor: study.difficulty === 'advanced' ? '#d32f2f' : (study.difficulty === 'intermediate' ? '#f57c00' : '#388e3c'), color: '#fff', padding: '0.25rem 0.75rem', borderRadius: '16px', fontSize: '0.75rem', fontWeight: '600', textTransform: 'capitalize' }}>{study.difficulty}</span>}
</div>
<h3>{study.title}</h3>
<p>{study.description}</p>
{study.estimatedHours && <p style={{ fontSize: '0.875rem', color: '#666', marginTop: '0.5rem' }}> {study.estimatedHours} hours</p>}
</div>
<div className="study-module-row-right">
<p className="study-module-focus">Track Snapshot</p>
<p>{study.sections.length > 0 ? `${study.sections.length} lessons available` : 'Lessons will be published soon.'}</p>
<p>{study.sections.length > 0 ? 'Estimated pace: 1-2 lessons per week' : 'Pacing details coming with first lesson release.'}</p>
</div>
</Link>
))}
</div>
</div>
</section>
{plannedStudies.length > 0 && (
<section className="section-study-module" aria-label="Upcoming studies">
<div className="section-inner">
<div className="study-module-header">
<p>Coming Soon</p>
<h2>Next Study Tracks</h2>
</div>
<div className="study-module-list">
{plannedStudies.map(study => (
<article key={study.id} className="study-module-row study-module-row--disabled" aria-disabled="true">
<div className="study-module-row-left">
<p className="study-module-lesson">Planned</p>
<h3>{study.title}</h3>
<p>{study.description}</p>
</div>
<div className="study-module-row-right">
<p className="study-module-focus">Status</p>
<p>Preparing lesson structure and media.</p>
</div>
</article>
))}
</div>
</div>
</section>
)}
</main>
)
}
export function ColossiansStudyIndexPage({ content }: Props) {
const { studySlug } = useParams<{ studySlug?: string }>()
const studies = getStudies(content)
const study = getStudyBySlug(studies, studySlug)
if (!study) {
return (
<main className="thanks-page" aria-label="Study not found">
<div className="thanks-card">
<p className="eyebrow">Study Hub</p>
<h1>Study not found</h1>
<p>The study you requested is not available yet.</p>
<Link to="/study" className="btn-primary">Back to Studies</Link>
</div>
</main>
)
}
const sections = study.sections
const chapterSummaries = getChapterSummaries(study)
const populatedChapterCount = chapterSummaries.filter(chapter => chapter.lessonCount > 0).length
return (
<main className="study-index-page" aria-label={`${study.title} study`}>
<section className="section-study-course-hero">
<div className="section-inner study-course-hero-inner">
<p className="eyebrow">Online Bible Class</p>
<h1>{study.title}</h1>
<p className="study-course-hero-copy">{study.description}</p>
<div className="study-course-meta">
<span>{sections.length} lessons</span>
<span>{chapterSummaries.length} chapters</span>
<span>{populatedChapterCount} chapters with lessons</span>
<span>Text + commentary + discussion</span>
<span>Student notes enabled</span>
</div>
<div className="study-course-hero-actions">
{sections.length > 0 && <Link to={`/study/${study.slug}/${sections[0].id}`} className="btn-primary">Start Class</Link>}
<Link to={`/study/${study.slug}/notes`} className="btn-secondary">My Notes</Link>
<Link to="/study" className="btn-secondary">Back to Studies</Link>
</div>
</div>
</section>
{sections.length === 0 && (
<section className="section-study-module">
<div className="section-inner">
<article className="study-module-row study-module-row--disabled" aria-disabled="true">
<div className="study-module-row-left">
<p className="study-module-lesson">Planned</p>
<h3>Lessons are being prepared</h3>
<p>Use the admin Studies editor to add section lessons and publish when ready.</p>
</div>
</article>
</div>
</section>
)}
{sections.length > 0 && (
<section className="section-study-module" aria-label="Course lessons">
<div className="section-inner">
<div className="study-module-header">
<p>Course Lessons</p>
<h2>{study.title}</h2>
</div>
<div className="study-module-list">
{sections.map(section => {
const lessonNumber = getLessonNumber(sections, section.id)
const isNew = isNewLesson(section)
const coming = isComingSoon(section)
const isDisabled = coming
return (
<Link key={section.id} to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`} className={`study-module-row${isDisabled ? ' study-module-row--disabled' : ''}`} onClick={e => isDisabled && e.preventDefault()}>
<div className="study-module-row-left">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<p className="study-module-lesson">Lesson {lessonNumber}</p>
{isNew && <span style={{ backgroundColor: '#4caf50', color: '#fff', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>New</span>}
{coming && <span style={{ backgroundColor: '#ffb74d', color: '#333', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Coming {getReleaseDateDisplay(section)}</span>}
</div>
<h3>{section.title}</h3>
<p>{section.summary}</p>
</div>
<div className="study-module-row-right">
<p className="study-section-reference">{section.reference}</p>
<p className="study-module-focus">Focus question</p>
<p>{getPrimaryQuestion(section.studyQuestions)}</p>
</div>
</Link>
)
})}
</div>
</div>
</section>
)}
</main>
)
}
export function ColossiansStudySectionPage({ content }: Props) {
const { studySlug, sectionId } = useParams<{ studySlug?: string; sectionId: string }>()
const studies = getStudies(content)
const study = getStudyBySlug(studies, studySlug)
const sections = study?.sections ?? []
const section = getSectionById(sections, sectionId)
const sectionIndex = sections.findIndex(item => item.id === sectionId)
const lessonNumber = getLessonNumber(sections, sectionId)
const previousSection = sectionIndex > 0 ? sections[sectionIndex - 1] : null
const nextSection = sectionIndex >= 0 && sectionIndex < sections.length - 1 ? sections[sectionIndex + 1] : null
const [auth, setAuth] = useState<StudyAuthState>({ checked: false, authenticated: false, username: '' })
const [usernameInput, setUsernameInput] = useState('')
const [passwordInput, setPasswordInput] = useState('')
const [authBusy, setAuthBusy] = useState(false)
const [authMessage, setAuthMessage] = useState('')
const [noteText, setNoteText] = useState('')
const [noteLoading, setNoteLoading] = useState(false)
const [noteSaving, setNoteSaving] = useState(false)
const [noteMessage, setNoteMessage] = useState('')
const canSaveNote = auth.authenticated && !noteSaving && !noteLoading
const lessonAudioEmbedUrl = useMemo(() => {
const trimmed = section?.audioEmbedUrl?.trim() ?? ''
if (!trimmed) return ''
return /^https?:\/\//i.test(trimmed) ? trimmed : ''
}, [section?.audioEmbedUrl])
const currentStudySlug = study?.slug ?? 'colossians'
const noteId = section?.id ? getNoteId(currentStudySlug, section.id) : ''
useEffect(() => {
document.title = section && study ? `${section.title} | ${study.title}` : 'Study'
}, [section, study])
useEffect(() => {
let cancelled = false
readJson<{ authenticated: boolean; username: string }>('/api/study-auth/status')
.then(data => {
if (cancelled) return
setAuth({ checked: true, authenticated: Boolean(data.authenticated), username: data.username ?? '' })
})
.catch(() => {
if (cancelled) return
setAuth({ checked: true, authenticated: false, username: '' })
})
return () => {
cancelled = true
}
}, [])
useEffect(() => {
if (!auth.authenticated || !noteId) {
setNoteText('')
return
}
let cancelled = false
setNoteLoading(true)
setNoteMessage('')
readJson<{ note: string }>(`/api/study-notes/${encodeURIComponent(noteId)}`)
.then(data => {
if (cancelled) return
setNoteText(data.note ?? '')
})
.catch(err => {
if (cancelled) return
setNoteMessage(err instanceof Error ? err.message : 'Unable to load your note.')
})
.finally(() => {
if (cancelled) return
setNoteLoading(false)
})
return () => {
cancelled = true
}
}, [auth.authenticated, noteId])
async function submitAuth(mode: 'login' | 'signup') {
setAuthBusy(true)
setAuthMessage('')
try {
const payload = { username: usernameInput, password: passwordInput }
const data = await readJson<{ username: string }>(`/api/study-auth/${mode}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
setAuth({ checked: true, authenticated: true, username: data.username ?? usernameInput.trim().toLowerCase() })
setUsernameInput('')
setPasswordInput('')
setAuthMessage(mode === 'signup' ? 'Account created. You can now save notes for each lesson.' : 'Signed in successfully.')
} catch (err) {
setAuthMessage(err instanceof Error ? err.message : 'Sign-in failed.')
} finally {
setAuthBusy(false)
}
}
async function logoutStudyUser() {
setAuthBusy(true)
setAuthMessage('')
try {
await readJson<{ ok: boolean }>('/api/study-auth/logout', { method: 'POST' })
setAuth({ checked: true, authenticated: false, username: '' })
setNoteText('')
setAuthMessage('Signed out.')
} catch (err) {
setAuthMessage(err instanceof Error ? err.message : 'Unable to sign out.')
} finally {
setAuthBusy(false)
}
}
async function saveNote() {
if (!noteId || !canSaveNote) return
setNoteSaving(true)
setNoteMessage('')
try {
const data = await readJson<{ ok: boolean; note: string }>(`/api/study-notes/${encodeURIComponent(noteId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ note: noteText }),
})
setNoteText(data.note ?? '')
setNoteMessage('Notes saved.')
} catch (err) {
setNoteMessage(err instanceof Error ? err.message : 'Unable to save note.')
} finally {
setNoteSaving(false)
}
}
if (!study || !section) {
return (
<main className="thanks-page" aria-label="Section not found">
<div className="thanks-card">
<p className="eyebrow">Study Hub</p>
<h1>Section not found</h1>
<p>The section you requested is not available yet.</p>
<Link to="/study" className="btn-primary">Back to Studies</Link>
</div>
</main>
)
}
return (
<main className="study-section-page" aria-label={section.title}>
<section className="section-study-classroom">
<div className="section-inner study-classroom-shell">
<div className="study-classroom-main">
<Link to={`/study/${study.slug}`} className="study-detail-back">Back to {study.title}</Link>
<p className="study-lesson-label">Lesson {lessonNumber} of {sections.length}</p>
<h1>{section.title}</h1>
<p className="study-detail-reference">{section.reference}</p>
<p className="study-detail-summary">{section.summary}</p>
{lessonAudioEmbedUrl && (
<article className="study-class-block">
<h2>Lesson Audio</h2>
<div className="study-audio-embed-wrap">
<iframe
src={lessonAudioEmbedUrl}
title={`${section.title} audio`}
width="100%"
height="152"
frameBorder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
/>
</div>
</article>
)}
<article className="study-class-block">
<h2>Scripture Text</h2>
<p className="study-detail-copy study-detail-copy--scripture">{section.passageText || `Add the passage text for ${section.reference} here when you move the guide online.`}</p>
</article>
<article className="study-class-block">
<h2>Instructor Commentary</h2>
<p className="study-detail-copy">{section.commentary}</p>
</article>
<article className="study-class-block">
<h2>Discussion Questions</h2>
<ol className="study-detail-list">
{section.studyQuestions.map((question, index) => (
<li key={index}>{question}</li>
))}
</ol>
</article>
</div>
<aside className="study-classroom-sidebar" aria-label="Lesson tools">
<article className="study-class-side-block">
<h3>Study Track</h3>
<p>{study.title}</p>
<p>{study.description}</p>
</article>
<article className="study-class-side-block">
<h3>Greek Word Study</h3>
{section.greekNotes.length > 0 ? (
<ul className="study-detail-list">
{section.greekNotes.map((note, index) => (
<li key={index}>{note}</li>
))}
</ul>
) : (
<p className="study-detail-copy">Greek notes will be added for this lesson.</p>
)}
</article>
<article className="study-class-side-block">
<h3>Student Notes</h3>
{!auth.checked && <p className="study-detail-copy">Checking sign-in status...</p>}
{auth.checked && !auth.authenticated && (
<div className="study-auth-box">
<label htmlFor="study-username">Username</label>
<input id="study-username" type="text" value={usernameInput} onChange={e => setUsernameInput(e.target.value)} placeholder="yourname" autoComplete="username" />
<label htmlFor="study-password">Password</label>
<input id="study-password" type="password" value={passwordInput} onChange={e => setPasswordInput(e.target.value)} placeholder="At least 8 characters" autoComplete="current-password" />
<div className="study-auth-actions">
<button type="button" className="btn-secondary" disabled={authBusy} onClick={() => submitAuth('login')}>Sign In</button>
<button type="button" className="btn-primary" disabled={authBusy} onClick={() => submitAuth('signup')}>Create Account</button>
</div>
</div>
)}
{auth.checked && auth.authenticated && (
<div className="study-notes-box">
<p className="study-notes-user">Signed in as {auth.username}</p>
<Link to={`/study/${study.slug}/notes`} className="btn-secondary">Open My Notes</Link>
<textarea
rows={8}
value={noteText}
onChange={e => setNoteText(e.target.value)}
placeholder="Write your lesson notes here..."
disabled={noteLoading || noteSaving}
/>
<div className="study-auth-actions">
<button type="button" className="btn-primary" disabled={!canSaveNote} onClick={saveNote}>{noteSaving ? 'Saving...' : 'Save Notes'}</button>
<button type="button" className="btn-secondary" disabled={authBusy} onClick={logoutStudyUser}>Sign Out</button>
</div>
</div>
)}
{(authMessage || noteMessage) && <p className="study-note-status">{authMessage || noteMessage}</p>}
</article>
<article className="study-class-side-block">
<h3>Lesson Navigation</h3>
<div className="study-detail-nav">
{previousSection ? (
<Link to={`/study/${study.slug}/${previousSection.id}`} className="btn-secondary">Previous Lesson</Link>
) : <span className="study-nav-placeholder" />}
{nextSection ? (
<Link to={`/study/${study.slug}/${nextSection.id}`} className="btn-secondary">Next Lesson</Link>
) : <span className="study-nav-placeholder" />}
</div>
</article>
</aside>
</div>
</section>
</main>
)
}
export function ColossiansStudyNotesPage({ content }: Props) {
const { studySlug } = useParams<{ studySlug?: string }>()
const studies = getStudies(content)
const study = getStudyBySlug(studies, studySlug)
const [auth, setAuth] = useState<StudyAuthState>({ checked: false, authenticated: false, username: '' })
const [notes, setNotes] = useState<StudyNotesMap>({})
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState('')
useEffect(() => {
document.title = study ? `My Notes | ${study.title}` : 'My Study Notes'
}, [study])
useEffect(() => {
let cancelled = false
async function load() {
try {
const authData = await readJson<{ authenticated: boolean; username: string }>('/api/study-auth/status')
if (cancelled) return
if (!authData.authenticated) {
setAuth({ checked: true, authenticated: false, username: '' })
setLoading(false)
return
}
setAuth({ checked: true, authenticated: true, username: authData.username ?? '' })
const notesData = await readJson<{ notes: StudyNotesMap }>('/api/study-notes')
if (cancelled) return
setNotes(notesData.notes ?? {})
} catch (err) {
if (cancelled) return
setMessage(err instanceof Error ? err.message : 'Unable to load notes.')
} finally {
if (cancelled) return
setLoading(false)
}
}
load()
return () => {
cancelled = true
}
}, [])
if (!study) {
return (
<main className="thanks-page" aria-label="Study not found">
<div className="thanks-card">
<p className="eyebrow">Study Hub</p>
<h1>Study not found</h1>
<p>The study you requested is not available yet.</p>
<Link to="/study" className="btn-primary">Back to Studies</Link>
</div>
</main>
)
}
const prefix = `${study.slug}--`
const entries = Object.entries(notes)
.filter(([key]) => key.startsWith(prefix))
.map(([key, note]) => {
const sectionId = key.slice(prefix.length)
const section = study.sections.find(item => item.id === sectionId)
return { sectionId, section, note }
})
.filter(item => item.note && item.note.trim())
return (
<main className="study-section-page" aria-label="My study notes">
<section className="section-study-classroom">
<div className="section-inner">
<Link to={`/study/${study.slug}`} className="study-detail-back">Back to {study.title}</Link>
<p className="study-lesson-label">Student Workspace</p>
<h1>My Lesson Notes</h1>
{!loading && auth.authenticated && <p className="study-detail-summary">Signed in as {auth.username}</p>}
{loading && <p className="study-detail-copy">Loading your notes...</p>}
{!loading && !auth.authenticated && (
<article className="study-class-block">
<h2>Sign In Required</h2>
<p className="study-detail-copy">Open any lesson and sign in from the Student Notes panel to see your saved notes here.</p>
{study.sections[0] && <Link to={`/study/${study.slug}/${study.sections[0].id}`} className="btn-primary">Open First Lesson</Link>}
</article>
)}
{!loading && auth.authenticated && entries.length === 0 && (
<article className="study-class-block">
<h2>No Notes Yet</h2>
<p className="study-detail-copy">You have not saved notes yet. Open a lesson and use the Student Notes area to start.</p>
{study.sections[0] && <Link to={`/study/${study.slug}/${study.sections[0].id}`} className="btn-primary">Open First Lesson</Link>}
</article>
)}
{!loading && auth.authenticated && entries.length > 0 && (
<div className="study-module-list">
{entries.map(({ sectionId, section, note }) => (
<article key={sectionId} className="study-module-row">
<div className="study-module-row-left">
<p className="study-module-lesson">{section ? `Lesson ${getLessonNumber(study.sections, section.id)}` : 'Saved Note'}</p>
<h3>{section?.title ?? sectionId}</h3>
<p>{section ? section.reference : 'Lesson reference unavailable'}</p>
</div>
<div className="study-module-row-right">
<p className="study-module-focus">Your Note</p>
<p className="study-detail-copy study-detail-copy--scripture">{note}</p>
{section && <Link to={`/study/${study.slug}/${section.id}`} className="btn-secondary">Open Lesson</Link>}
</div>
</article>
))}
</div>
)}
{message && <p className="study-note-status">{message}</p>}
</div>
</section>
</main>
)
}
+81
View File
@@ -0,0 +1,81 @@
export type ColossiansStudySection = {
id: string
chapter: number
reference: string
title: string
audioEmbedUrl?: string
passageText: string
summary: string
commentary: string
greekNotes: string[]
studyQuestions: string[]
releasedAt?: string
}
function section(
id: string,
chapter: number,
reference: string,
title: string,
audioEmbedUrl: string,
passageText: string,
summary: string,
commentary: string,
studyQuestions: string[],
greekNotes: string[] = [],
releasedAt: string | undefined = undefined,
): ColossiansStudySection {
return { id, chapter, reference, title, audioEmbedUrl, passageText, summary, commentary, studyQuestions, greekNotes, ...(releasedAt ? { releasedAt } : {}) }
}
export const DEFAULT_COLOSSIANS_STUDY_SECTIONS: ColossiansStudySection[] = [
section(
'1-1-2',
1,
'1:1-2',
'Paul\'s Greeting',
'',
'v. 1 Paul, an apostle of Christ Jesus by the will of God, and Timothy our brother,\n\n'
+ 'v. 2 To the saints in Colossae, the faithful brothers in Christ: Grace and peace to you from God our Father.',
"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.",
"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.",
['Why does Paul begin with grace and peace?', 'What tone does this create for the rest of the study?'],
[
'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.',
],
'2026-05-04T00:00:00Z',
),
section('1-3-8', 1, '1:3-8', 'Thanksgiving and Report', '', '', 'Paul thanks God for faith, hope, and love, and reports that the gospel is bearing fruit.', 'The gospel is shown to be active and alive. Paul points to evidence of real spiritual growth, not merely religious activity.', ['What signs of gospel growth does Paul mention?', 'How do faith, hope, and love work together here?'], [], '2026-05-04T00:00:00Z'),
section('1-9-14', 1, '1:9-14', 'Paul\'s Prayer', '', '', 'Paul prays for spiritual wisdom, endurance, and a life worthy of the Lord.', '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.', ['What does Paul ask God to produce?', 'How does this prayer shape the goals of the study?'], [], '2026-05-04T00:00:00Z'),
section('1-15-20', 1, '1:15-20', 'The Supremacy of Christ', '', '', 'Christ is supreme over creation, the church, and reconciliation.', 'This is the center of Colossians. Everything else in the letter depends on who Christ is and what He has done.', ['What stands out most about Christ\'s supremacy?', 'Why is this passage central to the study?'], [], '2026-05-04T00:00:00Z'),
section('1-21-23', 1, '1:21-23', 'Reconciliation Applied', '', '', 'Paul explains how Christ\'s work changes alienation into steadfast faith.', 'The gospel moves from doctrine to real life. Paul shows what reconciliation looks like for people who have been brought near to God.', ['How does Paul describe the believer\'s former condition?', 'What does continuing in the faith look like?'], [], '2026-05-18T00:00:00Z'),
section('1-24-29', 1, '1:24-29', 'Paul\'s Ministry and the Revealed Mystery', '', '', 'Paul describes his labor to proclaim Christ and present believers mature in Him.', 'Paul\'s ministry is costly and purposeful. He labors so the church will grow into maturity, not just receive information.', ['What is the goal of Paul\'s ministry?', 'Why is maturity such an important theme here?'], [], '2026-05-18T00:00:00Z'),
section('2-1-5', 2, '2:1-5', 'Paul\'s Pastoral Concern', '', '', 'Paul reveals his struggle for the believers and his desire for encouragement and unity.', 'This passage shows the heart behind the letter. Paul is deeply invested in the spiritual stability of the church.', ['What does Paul want the believers to experience?', 'Why does he emphasize encouragement and unity?'], [], '2026-06-01T00:00:00Z'),
section('2-6-8', 2, '2:6-8', 'Walk in Christ', '', '', 'Paul calls believers to continue in Christ and resist deceptive teaching.', 'The Christian life begins in Christ and must continue in Christ. Paul warns that subtle deception can slowly pull believers off course.', ['What does it mean to continue in Christ?', 'What kinds of deception should believers watch for?'], [], '2026-06-01T00:00:00Z'),
section('2-9-10', 2, '2:9-10', 'Supremacy and Sufficiency', '', '', 'The fullness of deity dwells in Christ, and believers are made complete in Him.', 'This section corrects the idea that anything must be added to Christ. He is fully sufficient for the believer\'s life and growth.', ['Why is fullness in Christ such an important correction?', 'How does this guard against spiritual insecurity?'], [], '2026-06-15T00:00:00Z'),
section('2-11-15', 2, '2:11-15', 'What Christ Has Done', '', '', 'Paul explains the believer\'s union with Christ in His death, resurrection, and victory.', 'The work of Christ is presented as decisive and complete. This becomes the foundation for all the commands that follow.', ['What changes because of Christ\'s work here?', 'How does victory over the powers encourage believers today?'], [], '2026-06-15T00:00:00Z'),
section('2-16-17', 2, '2:16-17', 'Freedom from Legalistic Judgment', '', '', 'Paul warns against letting others judge believers over shadows when Christ is the substance.', 'Outward practices can never replace Christ. Paul reminds the church that the real thing has come in Jesus.', ['What do shadows and substance mean here?', 'Where do believers still feel pressure from external judgment?'], [], '2026-06-29T00:00:00Z'),
section('2-18-19', 2, '2:18-19', 'Warning Against False Mysticism', '', '', 'Paul warns against prideful spirituality that disconnects believers from Christ the head.', 'Anything that pulls attention away from Christ and His body is spiritually dangerous. True growth stays connected to the head.', ['How does Paul describe false spirituality?', 'Why is connection to Christ essential?'], [], '2026-06-29T00:00:00Z'),
section('2-20-23', 2, '2:20-23', 'The Emptiness of Human Regulations', '', '', 'Human rules cannot produce true transformation.', 'External restrictions may look wise, but they cannot change the heart. Paul points the church back to union with Christ.', ['Why do human regulations fail to transform the heart?', 'What does real spiritual change require?'], [], '2026-07-13T00:00:00Z'),
section('3-1-4', 3, '3:1-4', 'Seek the Things Above', '', '', 'Paul calls believers to set their hearts and minds on Christ.', '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.', ['What does it mean to seek the things above?', 'How does hidden life in Christ shape daily priorities?'], [], '2026-07-13T00:00:00Z'),
section('3-5-9', 3, '3:5-9', 'Put to Death the Old Life', '', '', 'Paul calls believers to put away the practices of the old life.', 'Christian growth includes real moral change. Paul\'s language is strong because the old life cannot be carried into the new one.', ['Which old patterns does Paul name?', 'Why is decisive change necessary?'], [], '2026-07-27T00:00:00Z'),
section('3-10-11', 3, '3:10-11', 'Put On the New Self', '', '', 'Believers are renewed in Christ and formed into one new people.', '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.', ['What does renewal in Christ produce?', 'How does this reshape how believers see one another?'], [], '2026-07-27T00:00:00Z'),
section('3-12-14', 3, '3:12-14', 'Clothe Yourselves with Christlike Virtues', '', '', 'Paul lists the virtues believers should wear as Christ\'s character becomes visible in them.', 'The image of clothing makes virtue practical and visible. Paul wants Christ\'s character to define the way believers live together.', ['Which virtues are most needed in your context?', 'How does love bind these virtues together?'], [], '2026-08-10T00:00:00Z'),
section('3-15-17', 3, '3:15-17', 'The Rule of Christ\'s Peace and Word', '', '', 'Paul calls the church to let Christ\'s peace rule and His word dwell richly among them.', 'Peace, gratitude, and the word of Christ shape a healthy Christian community. These verses connect worship, teaching, and daily life.', ['What does it look like for Christ\'s peace to rule?', 'How can the word of Christ dwell richly in a church family?'], [], '2026-08-10T00:00:00Z'),
section('3-18-21', 3, '3:18-21', 'Household Code — Relationships', '', '', 'Paul applies the new life in Christ to family relationships.', 'The gospel reaches into ordinary relationships. Christ changes how homes are ordered and how people treat one another.', ['How do these instructions reflect Christ\'s lordship?', 'Which relationship is most challenging to apply today?'], [], '2026-08-24T00:00:00Z'),
section('3-22-25', 3, '3:22-25', 'Household Code — Work as unto the Lord', '', '', 'Paul instructs believers to serve faithfully and wholeheartedly.', 'Work becomes worship when it is done for Christ. Paul teaches integrity, diligence, and accountability before a heavenly Master.', ['How does serving the Lord reshape everyday work?', 'What does Paul say about fairness and accountability?'], [], '2026-08-24T00:00:00Z'),
section('4-1', 4, '4:1', 'Masters — Fairness Before a Heavenly Master', '', '', 'Paul reminds masters to act justly because they too answer to the Lord.', 'The gospel confronts authority as well as submission. All authority is accountable to Christ.', ['Why does Paul remind masters of their heavenly Master?', 'What does fairness look like in this context?'], [], '2026-09-07T00:00:00Z'),
section('4-2-6', 4, '4:2-6', 'Devoted to Prayer and Wise Witness', '', '', 'Paul closes with a call to steadfast prayer and wise witness toward outsiders.', 'Prayer and witness belong together. Paul shows that spiritual alertness and everyday speech are both part of faithful ministry.', ['How are prayer and witness connected here?', 'What does gracious speech look like in real life?'], [], '2026-09-07T00:00:00Z'),
section('4-7-9', 4, '4:7-9', 'Tychicus and Onesimus', '', '', 'Paul sends trusted messengers to encourage the church and bring news from the ministry field.', 'These greetings reveal the relational side of ministry. The letter is carried by real people who serve the church with loyalty and care.', ['Why do these messengers matter to the letter?', 'What does this tell us about trusted coworkers?'], [], '2026-09-21T00:00:00Z'),
section('4-10-14', 4, '4:10-14', 'Greetings from Paul\'s Co-workers', '', '', 'Paul names the coworkers who are with him and commends their ministry support.', 'The closing greetings show the breadth of the gospel network. Ministry is shared, supported, and strengthened by faithful co-workers.', ['What stands out about Paul\'s coworkers?', 'How does shared ministry strengthen the church?'], [], '2026-09-21T00:00:00Z'),
section('4-15-18', 4, '4:15-18', 'Final Instructions and Closing', '', '', 'Paul offers final greetings, instructions, and a closing reminder to complete the ministry entrusted to them.', 'The letter ends with community, responsibility, and grace. Paul\'s final words keep the church focused on the mission it has received.', ['What final responsibility does Paul place on the church?', 'How does the closing reinforce the letter\'s themes?'], [], '2026-10-05T00:00:00Z'),
]
+61
View File
@@ -1,3 +1,26 @@
import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './colossiansStudyData'
import type { ColossiansStudySection as ColossiansStudySectionData } from './colossiansStudyData'
export type ColossiansStudySection = ColossiansStudySectionData
export type StudySection = ColossiansStudySectionData
export interface StudyProgram {
id: string
slug: string
title: string
description: string
homepageEyebrow?: string
showOnHomepage?: boolean
showNewTag?: boolean
newTagLabel?: string
status: 'active' | 'planned'
difficulty: 'beginner' | 'intermediate' | 'advanced'
estimatedHours: number
completionBadge: string
numberOfChapters: number
sections: StudySection[]
}
export interface CustomLink { export interface CustomLink {
id: string id: string
label: string label: string
@@ -145,6 +168,9 @@ export interface SiteContent {
studyGuideDownloadUrl: string studyGuideDownloadUrl: string
studyGuideUrl: string studyGuideUrl: string
studyGuideAmazonButtonLabel: string studyGuideAmazonButtonLabel: string
// ── Colossians Study ──
studies: StudyProgram[]
colossiansStudySections: ColossiansStudySection[]
// ── Share ── // ── Share ──
shareHeading: string shareHeading: string
shareP: string shareP: string
@@ -256,6 +282,41 @@ export const DEFAULTS: SiteContent = {
studyGuideDownloadUrl: '', studyGuideDownloadUrl: '',
studyGuideUrl: 'https://a.co/d/01sG2tOJ', studyGuideUrl: 'https://a.co/d/01sG2tOJ',
studyGuideAmazonButtonLabel: 'Get it on Amazon', 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.',
homepageEyebrow: 'New Study',
showOnHomepage: true,
showNewTag: true,
newTagLabel: 'NEW',
status: 'active',
difficulty: 'intermediate',
estimatedHours: 12,
completionBadge: 'Colossians Completion',
numberOfChapters: 4,
sections: DEFAULT_COLOSSIANS_STUDY_SECTIONS,
},
{
id: 'study-titus',
slug: 'titus',
title: 'Titus: Sound Doctrine',
description: 'A full online class version of Titus with lesson-by-lesson notes and discussion.',
homepageEyebrow: 'Planned Study',
showOnHomepage: false,
showNewTag: false,
newTagLabel: 'NEW',
status: 'planned',
difficulty: 'beginner',
estimatedHours: 8,
completionBadge: 'Titus Completion',
numberOfChapters: 3,
sections: [],
},
],
colossiansStudySections: DEFAULT_COLOSSIANS_STUDY_SECTIONS,
shareHeading: 'Help one more person hear the Word this week.', 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.', shareP: 'Scan the QR code or text the show link to a friend who needs encouragement today.',
whereToNextEyebrow: 'Where to Next', whereToNextEyebrow: 'Where to Next',