Compare commits

...

2 commits

Author SHA1 Message Date
Benjamin Slingo 7ab47d6017 Clean up repository: remove shell scripts, test files, and temporary utilities
- Removed all .fish and .sh utility scripts
- Removed test SQL files and debug scripts
- Removed temp files and symlinks
- Removed HTML test files and Python migration helpers
- Updated .gitignore to prevent these files from being tracked again

Repository now contains only essential project code and migrations.
2025-08-19 21:00:09 -04:00
Benjamin Slingo b150e08163 Remove .serena folder from tracking and add to .gitignore 2025-08-19 20:57:56 -04:00
83 changed files with 35 additions and 12168 deletions

35
.gitignore vendored
View file

@ -63,3 +63,38 @@ debug_*.sql
# Service files
*.service
# Serena AI assistant files
.serena/
# Separate projects
church-website-axum/
# Shell scripts and utilities (keep only essential ones)
*.fish
*.sh
fix_*.py
*test*.sql
*debug*.sql
temp_*.txt
temp_*.sql
debug_*.fish
debug_*.sh
# Symlinks and temp files
claude
current
# HTML test files
*test*.html
# Binary files that shouldn't be versioned
*.apk
rtsda-android
# SQL migration helpers (keep actual migrations)
check_*.sql
clean_*.sql
force_*.sql
validate_*.sql
verify_*.sql

View file

@ -1,82 +0,0 @@
# Church API Code Style & Conventions
## Rust Code Style
### Naming Conventions
- **Functions**: `snake_case` (e.g., `get_video_duration_direct`, `transcode_hls_segment_ffmpeg`)
- **Types/Structs**: `PascalCase` (e.g., `StreamingTranscodingService`, `ChunkStreamingService`)
- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `TS_PACKET_SIZE`, `NUM_PACKETS`)
- **Variables**: `snake_case` (e.g., `source_path`, `media_item_id`)
- **Modules**: `snake_case` (e.g., `streaming_transcoding`, `chunk_streaming`)
### File Organization
- **Handlers**: `src/handlers/*.rs` - HTTP request handling
- **Services**: `src/services/*.rs` - Business logic layer
- **Models**: `src/models/*.rs` - Data models and database entities
- **Utils**: `src/utils/*.rs` - Shared utility functions
- **DB**: `src/db/*.rs` - Database access layer
### Error Handling
- Uses custom `ApiError` type with `Result<T>` return type
- Comprehensive error mapping with `.map_err()` for external errors
- Structured error messages with context
### Logging Style
```rust
// Informational with emoji indicators
tracing::info!("🚀 Using CLI ffmpeg with Intel QSV AV1 hardware decoding");
tracing::info!("✅ Segment {} transcoded successfully", segment_index);
// Warnings
tracing::warn!("⚠️ Creating placeholder segment {}", segment_index);
// Errors
tracing::error!("❌ Failed to transcode segment {}: {:?}", segment_index, e);
// Debug information
tracing::debug!("🎬 FFmpeg command: {:?}", cmd);
```
### Async/Await Patterns
- Extensive use of `async`/`await` with Tokio runtime
- Proper error propagation with `?` operator
- Background task spawning with `tokio::spawn()`
### Documentation
- Function-level documentation for public APIs
- Inline comments for complex logic
- TODO comments for known improvements needed
## Architecture Patterns
### Service Layer Pattern
- Services handle business logic
- Handlers are thin and focus on HTTP concerns only
- Clear separation between web layer and business logic
### Error Handling Strategy
```rust
// Convert external errors to ApiError
.map_err(|e| ApiError::Internal(format!("Failed to run ffmpeg: {}", e)))?
```
### Resource Management
- Use of `Arc<RwLock<>>` for shared mutable state
- Semaphores for limiting concurrent operations
- Proper cleanup in GStreamer pipelines
### Configuration
- Environment variables used extensively (`std::env::var`)
- Sensible defaults provided
- Hardware acceleration detection and fallbacks
## Performance Considerations
- Hardware acceleration preferred (VA-API, Intel QSV)
- Chunked/streaming processing for large media files
- Caching of transcoded segments
- Concurrent processing limits to prevent resource exhaustion
## Dependencies Management
- Clear separation of concerns in Cargo.toml
- Comments explaining dependency purposes
- Both FFmpeg and GStreamer maintained during transition period

View file

@ -1,41 +0,0 @@
# Church API Project Overview
## Purpose
The Church API is a Rust-based web service designed for church media management and streaming. The primary functionality includes:
- **Media Management**: Upload, organize, and manage church sermons and media content
- **Video Streaming**: Provide intelligent video streaming with adaptive codec support (AV1, HEVC, H.264)
- **User Authentication**: JWT-based authentication system
- **Database Integration**: PostgreSQL database with SQLx for data persistence
- **Email Services**: Automated email functionality for church communications
## Tech Stack
- **Language**: Rust (2021 edition)
- **Web Framework**: Axum 0.7 with async/await (Tokio runtime)
- **Database**: PostgreSQL with SQLx 0.7
- **Media Processing**:
- GStreamer bindings (0.22) for high-performance streaming
- FFmpeg bindings (ffmpeg-next 7.0) - being replaced with GStreamer
- **Authentication**: JWT (jsonwebtoken) + bcrypt for password hashing
- **Logging**: tracing + tracing-subscriber for structured logging
- **Testing**: Built-in Rust testing framework
## Key Features
1. **Smart Video Streaming**: Detects client capabilities and serves optimal codec (AV1 direct, HEVC, or H.264 with transcoding)
2. **Hardware Acceleration**: Uses Intel QSV and VA-API for efficient video processing
3. **Chunk-based Streaming**: Netflix-style 10-second segments for smooth playback
4. **Caching System**: Intelligent caching of transcoded video segments
5. **HLS Support**: HTTP Live Streaming for maximum compatibility
## Architecture
- **Services Layer**: Business logic for transcoding, streaming, media scanning
- **Handlers Layer**: HTTP request handlers using Axum
- **Models Layer**: Data models and database entities
- **Utils Layer**: Shared utilities (codec detection, validation, etc.)
## Current Performance Focus
The project is actively migrating from FFmpeg CLI calls to native GStreamer bindings to:
- Eliminate subprocess overhead
- Reduce buffering and latency
- Improve hardware acceleration utilization
- Enable better error handling and resource management

View file

@ -1,114 +0,0 @@
# Church API Development Commands
## Core Development Commands
### Building & Testing
```bash
# Build the project
cargo build
# Build with release optimizations
cargo build --release
# Run tests
cargo test
# Run specific test module with output
cargo test images::tests -- --nocapture
# Check code without building
cargo check
# Format code
cargo fmt
# Run clippy linter
cargo clippy
```
### Running the Application
```bash
# Run in development mode (from src/main.rs)
cargo run
# Run the named binary
cargo run --bin church-api
# Run with environment variables
RUST_LOG=debug cargo run
```
### Database Management
```bash
# The project uses SQLx with PostgreSQL
# Migration files are likely in the migrations/ directory
# Check for database setup in .env files
```
### System Integration
```bash
# The project includes systemd service management
sudo systemctl restart church-api
sudo systemctl status church-api
# Logs can be viewed with
journalctl -fu church-api
```
### Media Processing
```bash
# The project uses both FFmpeg and GStreamer
# Check Intel Media Stack environment:
export LIBVA_DRIVER_NAME=iHD
export LIBVA_DRIVERS_PATH=/opt/intel/media/lib64
# Check hardware acceleration support
vainfo
intel_gpu_top
```
### Testing & Debugging Scripts
```bash
# Various test scripts are available:
./test.sh # General testing
./test_images.sh # Image processing tests
./test_media_system.sh # Media system tests
./comprehensive_test.sh # Full system tests
./server_debug.sh # Server debugging
# Church-specific scripts:
./church-api-script.sh # API management
./bible_verse.sh # Bible verse functionality
```
### File System Organization
```bash
# Uploads directory
ls -la uploads/
# Configuration
cat .env
cat .env.example
# Service files
ls -la *.service
# Migration and backup files
ls -la migrations/
ls -la backup_before_*/
```
### Development Workflow
1. Make changes to code
2. Run `cargo check` for quick syntax validation
3. Run `cargo test` to ensure tests pass
4. Run `cargo build` to compile
5. Test with relevant `test_*.sh` scripts
6. Deploy with `sudo systemctl restart church-api`
## Key Directories
- `src/` - Rust source code
- `migrations/` - Database migrations
- `uploads/` - Media file storage
- `templates/` - HTML templates
- `tests/` - Test files

View file

@ -1,108 +0,0 @@
# Task Completion Workflow
## When a coding task is completed, follow these steps:
### 1. Code Quality Checks
```bash
# Format the code
cargo fmt
# Run linter
cargo clippy
# Check for compilation errors
cargo check
```
### 2. Build & Test
```bash
# Build the project
cargo build
# Run tests
cargo test
# Run specific tests if relevant
cargo test module_name -- --nocapture
```
### 3. Media System Testing (if relevant)
```bash
# Test media processing
./test_media_system.sh
# Test image processing (if modified)
./test_images.sh
# Run comprehensive tests
./comprehensive_test.sh
```
### 4. Service Integration Testing
```bash
# Restart the service
sudo systemctl restart church-api
# Check service status
sudo systemctl status church-api
# View logs for errors
journalctl -fu church-api --lines=50
```
### 5. API Testing (if relevant)
```bash
# Test authentication
curl -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "..."}'
# Test relevant endpoints with JWT token
# (Check fix_routes.sh for examples)
```
### 6. Performance Validation (for media changes)
- Check hardware acceleration is working:
```bash
vainfo
intel_gpu_top # During transcoding
```
- Monitor memory usage and CPU utilization
- Verify transcoding times are reasonable
- Check for memory leaks in long-running operations
### 7. Documentation Updates
- Update inline comments for complex changes
- Add tracing logs for new operations
- Update memory files if architecture changes
### 8. Final Checklist
- [ ] Code compiles without warnings
- [ ] Tests pass
- [ ] Service restarts successfully
- [ ] No memory leaks or resource exhaustion
- [ ] Hardware acceleration functional (if applicable)
- [ ] Logging provides adequate debugging information
- [ ] Error handling is comprehensive
## Critical Notes
### For Media/Streaming Changes:
- Always test with actual video files
- Verify both AV1 and H.264 codecs work
- Check HLS playlist generation
- Test with different client user agents
- Monitor segment caching behavior
### For GStreamer Integration:
- Ensure GStreamer initialization succeeds
- Test pipeline cleanup (no resource leaks)
- Verify hardware acceleration paths
- Check error handling for missing plugins
- Test with various input formats
### Performance Requirements:
- Transcoding should complete faster than real-time
- Memory usage should remain stable
- No blocking of other requests during transcoding
- Proper cleanup of temporary files and resources

View file

@ -1,68 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: rust
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed)on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "church-api"

View file

@ -1,71 +0,0 @@
#!/usr/bin/env fish
echo "🔧 FIXING API TO SUPPORT IMAGE_PATH UPDATES"
echo "============================================"
# Check if we're in the right directory
if not test -f "src/models.rs"
echo "❌ Error: src/models.rs not found. Are you in the church-api directory?"
exit 1
end
echo "1⃣ Backing up original files..."
cp src/models.rs src/models.rs.backup
cp src/db/events.rs src/db/events.rs.backup
echo "✅ Backups created: .backup files"
echo "2⃣ Adding image_path to CreateEventRequest struct..."
sed -i 's/pub recurring_type: Option<String>,/pub recurring_type: Option<String>,\n pub image_path: Option<String>,/' src/models.rs
if grep -q "pub image_path: Option<String>," src/models.rs
echo "✅ Added image_path field to CreateEventRequest"
else
echo "❌ Failed to add image_path field"
exit 1
end
echo "3⃣ Updating database update function..."
# Replace the UPDATE query to include image_path
sed -i 's/recurring_type = $9, updated_at = NOW()/recurring_type = $9, image_path = $10, updated_at = NOW()/' src/db/events.rs
sed -i 's/WHERE id = $10/WHERE id = $11/' src/db/events.rs
sed -i '/req.recurring_type,/a\ req.image_path,' src/db/events.rs
if grep -q "image_path = \$10" src/db/events.rs
echo "✅ Updated database function"
else
echo "❌ Failed to update database function"
exit 1
end
echo "4⃣ Building the project..."
if cargo build
echo "✅ Build successful!"
else
echo "❌ Build failed! Restoring backups..."
cp src/models.rs.backup src/models.rs
cp src/db/events.rs.backup src/db/events.rs
exit 1
end
echo "5⃣ Showing changes made..."
echo ""
echo "=== Changes to src/models.rs ==="
diff src/models.rs.backup src/models.rs || true
echo ""
echo "=== Changes to src/db/events.rs ==="
diff src/db/events.rs.backup src/db/events.rs || true
echo ""
echo "🎉 SUCCESS!"
echo "============"
echo "✅ Added image_path field to CreateEventRequest struct"
echo "✅ Updated database update function to handle image_path"
echo "✅ Project compiled successfully"
echo ""
echo "🚀 Next steps:"
echo "1. Restart your API server"
echo "2. Run your image_path update script"
echo "3. Images should now load properly!"
echo ""
echo "💾 Backup files saved as:"
echo " - src/models.rs.backup"
echo " - src/db/events.rs.backup"

View file

@ -1,83 +0,0 @@
#!/bin/bash
# Add BibleVerse model to models.rs
cat >> src/models.rs << 'EOF'
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BibleVerse {
pub id: Uuid,
pub reference: String,
pub text: String,
pub is_active: bool,
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
EOF
# Create handlers/bible_verses.rs
cat > src/handlers/bible_verses.rs << 'EOF'
use crate::{db, error::Result, models::{ApiResponse, BibleVerse}, AppState};
use axum::{extract::State, Json};
pub async fn random(
State(state): State<AppState>,
) -> Result<Json<ApiResponse<BibleVerse>>> {
let verse = db::bible_verses::get_random(&state.pool).await?;
Ok(Json(ApiResponse {
success: true,
data: verse,
message: None,
}))
}
pub async fn list(
State(state): State<AppState>,
) -> Result<Json<ApiResponse<Vec<BibleVerse>>>> {
let verses = db::bible_verses::list(&state.pool).await?;
Ok(Json(ApiResponse {
success: true,
data: Some(verses),
message: None,
}))
}
EOF
# Create db/bible_verses.rs
cat > src/db/bible_verses.rs << 'EOF'
use sqlx::PgPool;
use uuid::Uuid;
use crate::{error::Result, models::BibleVerse};
pub async fn get_random(pool: &PgPool) -> Result<Option<BibleVerse>> {
let verse = sqlx::query_as!(
BibleVerse,
"SELECT * FROM bible_verses WHERE is_active = true ORDER BY RANDOM() LIMIT 1"
)
.fetch_optional(pool)
.await?;
Ok(verse)
}
pub async fn list(pool: &PgPool) -> Result<Vec<BibleVerse>> {
let verses = sqlx::query_as!(
BibleVerse,
"SELECT * FROM bible_verses WHERE is_active = true ORDER BY reference"
)
.fetch_all(pool)
.await?;
Ok(verses)
}
EOF
# Add module to handlers/mod.rs
echo "pub mod bible_verses;" >> src/handlers/mod.rs
# Add module to db/mod.rs
echo "pub mod bible_verses;" >> src/db/mod.rs
echo "✅ Bible verses files created!"
echo "Don't forget to add the routes to main.rs:"
echo '.route("/api/bible_verses/random", get(handlers::bible_verses::random))'
echo '.route("/api/bible_verses", get(handlers::bible_verses::list))'

View file

@ -1,51 +0,0 @@
#!/bin/bash
echo "=== CLEANING UP REMAINING PLACEHOLDERS ==="
# Check if these functions are used anywhere
echo "1. Checking if placeholder functions are used in routes..."
ROUTES_USING_CONFIG_LIST=$(grep -r "config::list" src/main.rs | wc -l)
ROUTES_USING_FILES=$(grep -r "files::" src/main.rs | wc -l)
echo "Routes using config::list: $ROUTES_USING_CONFIG_LIST"
echo "Routes using files handler: $ROUTES_USING_FILES"
# Remove the unused config list function
echo "2. Removing unused config list function..."
sed -i '/Config list - implement as needed/,/^}/d' src/handlers/config.rs
# Remove the files handler entirely if it's not used
echo "3. Removing unused files handler..."
rm -f src/handlers/files.rs
# Remove files from handlers mod.rs if it exists
echo "4. Cleaning up module references..."
sed -i '/mod files;/d' src/handlers/mod.rs 2>/dev/null || true
# Check our work
echo "5. Checking for remaining placeholders..."
REMAINING_PLACEHOLDERS=$(grep -r "implement as needed\|TODO\|Working\|TBA" src/ 2>/dev/null | wc -l)
echo "Remaining placeholders: $REMAINING_PLACEHOLDERS"
if [ $REMAINING_PLACEHOLDERS -eq 0 ]; then
echo "✅ All placeholders removed!"
else
echo "⚠️ Still have placeholders:"
grep -r "implement as needed\|TODO\|Working\|TBA" src/ 2>/dev/null
fi
# Build to make sure nothing broke
echo "6. Building to verify everything still works..."
cargo build --release
if [ $? -eq 0 ]; then
echo "✅ Build successful - API is clean and working!"
# Restart service
echo "7. Restarting service..."
sudo systemctl restart church-api
echo "🎉 YOUR CHURCH API IS NOW 100% COMPLETE WITH NO PLACEHOLDERS!"
else
echo "❌ Build failed - check for errors"
fi

View file

@ -1,22 +0,0 @@
#!/bin/bash
echo "🔍 CHECKING ACTUAL MODEL STRUCTURE"
echo "=================================="
echo "📋 SubmitEventRequest fields:"
grep -A 20 "pub struct SubmitEventRequest" src/models.rs || grep -A 20 "struct SubmitEventRequest" src/models.rs
echo ""
echo "📋 ApiError variants:"
grep -A 10 "pub enum ApiError" src/error.rs || grep -A 10 "enum ApiError" src/error.rs
echo ""
echo "📋 Database schema for pending_events:"
find . -name "*.sql" -exec grep -l "pending_events" {} \; | head -1 | xargs cat 2>/dev/null || echo "No migration files found"
echo ""
echo "🎯 What we need to do:"
echo "1. Use the ACTUAL fields from SubmitEventRequest"
echo "2. Use proper DateTime types"
echo "3. Use correct ApiError variants"
echo "4. Check if image/thumbnail fields exist in DB"

View file

@ -1,25 +0,0 @@
-- Check the specific bulletin that the API is returning
SELECT id, title, date,
length(scripture_reading) as scripture_length,
substring(scripture_reading, 1, 200) as scripture_sample,
CASE WHEN scripture_reading LIKE '%<%' THEN 'HAS HTML' ELSE 'CLEAN' END as has_html
FROM bulletins
WHERE id = '192730b5-c11c-4513-a37d-2a8b320136a4';
-- Let's also clean this specific record if it has HTML
UPDATE bulletins
SET scripture_reading = REGEXP_REPLACE(scripture_reading, '<[^>]*>', '', 'g'),
sabbath_school = REGEXP_REPLACE(COALESCE(sabbath_school, ''), '<[^>]*>', '', 'g'),
divine_worship = REGEXP_REPLACE(COALESCE(divine_worship, ''), '<[^>]*>', '', 'g'),
sunset = REGEXP_REPLACE(COALESCE(sunset, ''), '<[^>]*>', '', 'g')
WHERE id = '192730b5-c11c-4513-a37d-2a8b320136a4'
AND (scripture_reading LIKE '%<%'
OR sabbath_school LIKE '%<%'
OR divine_worship LIKE '%<%'
OR sunset LIKE '%<%');
-- Verify after cleaning
SELECT 'After targeted cleaning:' as status;
SELECT substring(scripture_reading, 1, 200) as cleaned_content
FROM bulletins
WHERE id = '192730b5-c11c-4513-a37d-2a8b320136a4';

View file

@ -1,235 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Netflix-Style Chunk Streaming Test</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #111;
color: #fff;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.status {
background: #333;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
font-family: monospace;
}
.chunk-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 5px;
margin: 20px 0;
}
.chunk {
padding: 10px;
text-align: center;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.chunk.not-started { background: #666; }
.chunk.transcoding { background: #f39c12; animation: pulse 1s infinite; }
.chunk.cached { background: #27ae60; }
.chunk.failed { background: #e74c3c; }
.chunk.current { border: 3px solid #fff; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
button {
background: #e50914;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover { background: #f40612; }
button:disabled { background: #666; cursor: not-allowed; }
.controls {
margin: 20px 0;
text-align: center;
}
.log {
background: #222;
padding: 10px;
border-radius: 5px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.success { color: #27ae60; }
.error { color: #e74c3c; }
.info { color: #3498db; }
</style>
</head>
<body>
<div class="container">
<h1>🍿 Netflix-Style Chunk Streaming Test</h1>
<div class="status" id="status">
Click "Load Video Info" to start
</div>
<div class="controls">
<button onclick="loadVideoInfo()">Load Video Info</button>
<button onclick="testChunk()" id="testBtn" disabled>Test Random Chunk</button>
<button onclick="testSequential()" id="seqBtn" disabled>Test Sequential</button>
<button onclick="clearLog()">Clear Log</button>
</div>
<div class="chunk-grid" id="chunkGrid"></div>
<div class="log" id="log"></div>
</div>
<script>
const API_BASE = window.location.origin;
const MEDIA_ID = '123e4567-e89b-12d3-a456-426614174000'; // Test UUID
let videoInfo = null;
let chunkStatus = {};
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
const className = type;
logDiv.innerHTML += `<div class="${className}">[${timestamp}] ${message}</div>`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
function updateStatus(message) {
document.getElementById('status').textContent = message;
}
async function loadVideoInfo() {
try {
log('Loading video info...', 'info');
const response = await fetch(`${API_BASE}/api/media/stream/${MEDIA_ID}/info`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
videoInfo = await response.json();
log(`✅ Video loaded: ${videoInfo.total_duration}s, ${videoInfo.num_segments} chunks`, 'success');
updateStatus(`Video: ${Math.round(videoInfo.total_duration)}s (${videoInfo.num_segments} chunks of ${videoInfo.segment_duration}s each)`);
// Enable buttons
document.getElementById('testBtn').disabled = false;
document.getElementById('seqBtn').disabled = false;
// Create chunk grid
createChunkGrid();
} catch (error) {
log(`❌ Failed to load video info: ${error.message}`, 'error');
updateStatus('Failed to load video info');
}
}
function createChunkGrid() {
const grid = document.getElementById('chunkGrid');
grid.innerHTML = '';
for (let i = 0; i < videoInfo.num_segments; i++) {
const chunk = document.createElement('div');
chunk.className = 'chunk not-started';
chunk.textContent = `${i}`;
chunk.title = `Chunk ${i} (${i * videoInfo.segment_duration}-${(i + 1) * videoInfo.segment_duration}s)`;
chunk.onclick = () => testSpecificChunk(i);
grid.appendChild(chunk);
chunkStatus[i] = 'not-started';
}
}
function updateChunkStatus(index, status) {
chunkStatus[index] = status;
const chunk = document.querySelector(`#chunkGrid .chunk:nth-child(${index + 1})`);
if (chunk) {
chunk.className = `chunk ${status}`;
}
}
async function testSpecificChunk(index) {
try {
updateChunkStatus(index, 'transcoding');
log(`🎬 Testing chunk ${index}...`, 'info');
const startTime = performance.now();
const response = await fetch(`${API_BASE}/api/media/stream/${MEDIA_ID}/chunk/${index}`);
const endTime = performance.now();
if (response.status === 202) {
// Still transcoding
const info = await response.json();
log(`⏳ Chunk ${index} transcoding (ETA: ${info.estimated_seconds}s)`, 'info');
// Poll for completion
setTimeout(() => testSpecificChunk(index), 2000);
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const contentLength = response.headers.get('content-length');
const contentType = response.headers.get('content-type');
const isCached = response.headers.get('x-chunk-cached') === 'true';
updateChunkStatus(index, 'cached');
log(`✅ Chunk ${index}: ${Math.round(endTime - startTime)}ms, ${contentLength} bytes, ${contentType} ${isCached ? '(cached)' : '(transcoded)'}`, 'success');
} catch (error) {
updateChunkStatus(index, 'failed');
log(`❌ Chunk ${index} failed: ${error.message}`, 'error');
}
}
async function testChunk() {
if (!videoInfo) return;
const randomIndex = Math.floor(Math.random() * videoInfo.num_segments);
await testSpecificChunk(randomIndex);
}
let sequentialIndex = 0;
async function testSequential() {
if (!videoInfo) return;
if (sequentialIndex >= videoInfo.num_segments) {
log('🎉 Sequential test completed!', 'success');
sequentialIndex = 0;
return;
}
await testSpecificChunk(sequentialIndex);
sequentialIndex++;
// Continue after a short delay
setTimeout(testSequential, 1000);
}
// Load video info on page load
window.addEventListener('load', loadVideoInfo);
</script>
</body>
</html>

View file

@ -1,525 +0,0 @@
#!/bin/bash
# Church API Deployment Script
# Run this script to deploy the complete Church API
set -e
echo "🦀 Starting Church API Deployment..."
# Configuration
PROJECT_DIR="/opt/rtsda/church-api"
DB_NAME="church_db"
DB_USER="postgres"
SERVICE_PORT="3002"
# Create project directory
echo "📁 Creating project directory..."
sudo mkdir -p $PROJECT_DIR
sudo chown $USER:$USER $PROJECT_DIR
cd $PROJECT_DIR
# Initialize Cargo project
echo "🦀 Initializing Rust project..."
cargo init --name church-api
# Create directory structure
mkdir -p src/{handlers,db} templates migrations uploads/{bulletins,events,avatars}
# Create Cargo.toml
echo "📦 Setting up dependencies..."
cat > Cargo.toml << 'EOF'
[package]
name = "church-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dotenv = "0.15"
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
jsonwebtoken = "9"
bcrypt = "0.15"
multer = "3.0"
mime_guess = "2.0"
lettre = "0.11"
askama = "0.12"
EOF
# Create .env file
echo "🔧 Creating environment configuration..."
cat > .env << 'EOF'
DATABASE_URL=postgresql://postgres:yourpassword@localhost/church_db
JWT_SECRET=change_this_super_secret_jwt_key_in_production_very_long_and_secure
RUST_LOG=info
UPLOAD_DIR=/opt/rtsda/church-api/uploads
SERVER_PORT=3002
# SMTP Configuration - Your Fastmail settings with proper church emails
SMTP_HOST=smtp.fastmail.com
SMTP_PORT=587
SMTP_USER=ben@slingoapps.dev
SMTP_PASS=9a9g5g7f2c8u233r
SMTP_FROM=noreply@rockvilletollandsda.church
ADMIN_EMAIL=admin@rockvilletollandsda.church
EOF
chmod 600 .env
echo "⚠️ IMPORTANT: Update the .env file with your actual SMTP credentials!"
echo "⚠️ Also update the database password in DATABASE_URL"
# Create main.rs
echo "📝 Creating main application..."
cat > src/main.rs << 'EOF'
use axum::{
routing::{get, post, put, delete},
Router,
extract::State,
middleware,
};
use dotenv::dotenv;
use sqlx::PgPool;
use std::{env, sync::Arc};
use tower_http::{cors::CorsLayer, services::ServeDir};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod error;
mod auth;
mod models;
mod handlers;
mod db;
mod email;
use error::Result;
use auth::auth_middleware;
use email::{EmailConfig, Mailer};
#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
pub jwt_secret: String,
pub upload_dir: String,
pub mailer: Arc<Mailer>,
}
#[tokio::main]
async fn main() -> Result<()> {
dotenv().ok();
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set");
let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET not set");
let upload_dir = env::var("UPLOAD_DIR").unwrap_or_else(|_| "./uploads".to_string());
let port = env::var("SERVER_PORT").unwrap_or_else(|_| "3002".to_string());
// Create upload directories
tokio::fs::create_dir_all(&format!("{}/bulletins", upload_dir)).await?;
tokio::fs::create_dir_all(&format!("{}/events", upload_dir)).await?;
tokio::fs::create_dir_all(&format!("{}/avatars", upload_dir)).await?;
let pool = PgPool::connect(&database_url).await?;
// Set up email
let email_config = EmailConfig::from_env()?;
let mailer = Arc::new(Mailer::new(email_config)?);
let state = AppState {
pool,
jwt_secret,
upload_dir: upload_dir.clone(),
mailer,
};
let app = Router::new()
// Public routes
.route("/api/bulletins", get(handlers::bulletins::list))
.route("/api/bulletins/current", get(handlers::bulletins::current))
.route("/api/bulletins/:id", get(handlers::bulletins::get))
.route("/api/events", get(handlers::events::list))
.route("/api/events/upcoming", get(handlers::events::upcoming))
.route("/api/events/featured", get(handlers::events::featured))
.route("/api/events/:id", get(handlers::events::get))
.route("/api/events/submit", post(handlers::events::submit))
.route("/api/church/config", get(handlers::config::get))
.route("/api/church/schedules", get(handlers::config::get_schedules))
.route("/api/app/version/:platform", get(handlers::config::get_app_version))
// Auth routes
.route("/api/auth/login", post(handlers::auth::login))
// Protected admin routes
.route("/api/bulletins", post(handlers::bulletins::create))
.route("/api/bulletins/:id", put(handlers::bulletins::update).delete(handlers::bulletins::delete))
.route("/api/events", post(handlers::events::create))
.route("/api/events/:id", put(handlers::events::update).delete(handlers::events::delete))
.route("/api/events/pending", get(handlers::events::list_pending))
.route("/api/events/pending/:id/approve", put(handlers::events::approve))
.route("/api/events/pending/:id/reject", put(handlers::events::reject))
.route("/api/church/config", put(handlers::config::update))
.route("/api/church/schedules", put(handlers::config::update_schedules))
.route("/api/files/upload", post(handlers::files::upload))
.route("/api/users", get(handlers::auth::list_users))
.route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware))
// File serving
.nest_service("/uploads", ServeDir::new(&upload_dir))
.layer(CorsLayer::permissive())
.with_state(state);
let addr = format!("0.0.0.0:{}", port);
tracing::info!("Church API listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
EOF
# Create ALL the source files...
echo "📝 Creating source files..."
# error.rs
cat > src/error.rs << 'EOF'
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
#[derive(Debug)]
pub enum ApiError {
DatabaseError(sqlx::Error),
AuthError(String),
ValidationError(String),
NotFound(String),
FileError(std::io::Error),
JwtError(jsonwebtoken::errors::Error),
BcryptError(bcrypt::BcryptError),
SerdeError(serde_json::Error),
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let (status, message) = match self {
ApiError::DatabaseError(e) => {
tracing::error!("Database error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string())
}
ApiError::AuthError(msg) => (StatusCode::UNAUTHORIZED, msg),
ApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
ApiError::FileError(e) => {
tracing::error!("File error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "File operation failed".to_string())
}
ApiError::JwtError(e) => {
tracing::error!("JWT error: {:?}", e);
(StatusCode::UNAUTHORIZED, "Invalid token".to_string())
}
ApiError::BcryptError(e) => {
tracing::error!("Bcrypt error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Password hashing error".to_string())
}
ApiError::SerdeError(e) => {
tracing::error!("Serde error: {:?}", e);
(StatusCode::BAD_REQUEST, "Invalid JSON".to_string())
}
};
(
status,
Json(json!({
"success": false,
"error": message
})),
)
.into_response()
}
}
impl From<sqlx::Error> for ApiError {
fn from(error: sqlx::Error) -> Self {
ApiError::DatabaseError(error)
}
}
impl From<std::io::Error> for ApiError {
fn from(error: std::io::Error) -> Self {
ApiError::FileError(error)
}
}
impl From<jsonwebtoken::errors::Error> for ApiError {
fn from(error: jsonwebtoken::errors::Error) -> Self {
ApiError::JwtError(error)
}
}
impl From<bcrypt::BcryptError> for ApiError {
fn from(error: bcrypt::BcryptError) -> Self {
ApiError::BcryptError(error)
}
}
impl From<serde_json::Error> for ApiError {
fn from(error: serde_json::Error) -> Self {
ApiError::SerdeError(error)
}
}
pub type Result<T> = std::result::Result<T, ApiError>;
EOF
# I'll continue with the essential files to get you started quickly...
# The rest will be created as minimal working versions
echo "📝 Creating simplified working files..."
# Create a minimal working version for now
cat > src/models.rs << 'EOF'
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct User {
pub id: Uuid,
pub username: String,
pub email: Option<String>,
pub role: String,
}
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub message: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub token: String,
pub user: User,
}
EOF
# Create minimal handlers
cat > src/handlers/mod.rs << 'EOF'
pub mod auth;
pub mod bulletins;
pub mod events;
pub mod config;
pub mod files;
EOF
# Basic auth handler
cat > src/handlers/auth.rs << 'EOF'
use axum::{extract::State, Json};
use crate::{models::{LoginRequest, LoginResponse, ApiResponse}, AppState, error::Result};
pub async fn login(
State(_state): State<AppState>,
Json(_req): Json<LoginRequest>,
) -> Result<Json<ApiResponse<String>>> {
Ok(Json(ApiResponse {
success: true,
data: Some("Login endpoint - implement me!".to_string()),
message: Some("TODO".to_string()),
}))
}
pub async fn list_users(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> {
Ok(Json(ApiResponse {
success: true,
data: Some("Users endpoint - implement me!".to_string()),
message: None,
}))
}
EOF
# Create stub handlers for the rest
for handler in bulletins events config files; do
cat > src/handlers/${handler}.rs << EOF
use axum::{extract::State, Json};
use crate::{models::ApiResponse, AppState, error::Result};
pub async fn list(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> {
Ok(Json(ApiResponse {
success: true,
data: Some("${handler} endpoint - implement me!".to_string()),
message: None,
}))
}
// Add other stub functions as needed
pub async fn get(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn create(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn update(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn delete(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn current(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn upcoming(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn featured(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn submit(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn list_pending(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn approve(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn reject(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn get_schedules(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn update_schedules(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn get_app_version(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
pub async fn upload(State(_state): State<AppState>) -> Result<Json<ApiResponse<String>>> { list(_state).await }
EOF
done
# Create stub db modules
cat > src/db/mod.rs << 'EOF'
pub mod users;
pub mod bulletins;
pub mod events;
pub mod config;
EOF
for db in users bulletins events config; do
cat > src/db/${db}.rs << 'EOF'
// Stub database module - implement me!
EOF
done
# Create stub auth module
cat > src/auth.rs << 'EOF'
use axum::{extract::{Request, State}, middleware::Next, response::Response};
use crate::{error::ApiError, AppState};
pub async fn auth_middleware(
State(_state): State<AppState>,
request: Request,
next: Next,
) -> Result<Response, ApiError> {
// Stub auth middleware - implement me!
Ok(next.run(request).await)
}
EOF
# Create stub email module
cat > src/email.rs << 'EOF'
use std::env;
use crate::error::Result;
#[derive(Clone)]
pub struct EmailConfig {
pub smtp_host: String,
}
impl EmailConfig {
pub fn from_env() -> Result<Self> {
Ok(EmailConfig {
smtp_host: env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string()),
})
}
}
pub struct Mailer;
impl Mailer {
pub fn new(_config: EmailConfig) -> Result<Self> {
Ok(Mailer)
}
}
EOF
# Create basic database schema
cat > migrations/001_initial_schema.sql << 'EOF'
-- Basic users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'user',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Insert default admin user (password: 'admin123')
INSERT INTO users (username, email, password_hash, role) VALUES
('admin', 'admin@rockvilletollandsda.church', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewHhOQY.S1KElH0y', 'admin');
EOF
# Create systemd service
echo "🔧 Creating systemd service..."
sudo tee /etc/systemd/system/church-api.service > /dev/null << EOF
[Unit]
Description=Church API Service
After=network.target postgresql.service
[Service]
Type=simple
User=$USER
Group=$USER
WorkingDirectory=$PROJECT_DIR
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=$PROJECT_DIR/target/release/church-api
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Setup database
echo "🗄️ Setting up database..."
sudo -u postgres createdb $DB_NAME 2>/dev/null || echo "Database $DB_NAME already exists"
sudo -u postgres psql -d $DB_NAME -f migrations/001_initial_schema.sql
# Build the project
echo "🦀 Building Rust project..."
cargo build --release
# Enable and start service
echo "🚀 Starting service..."
sudo systemctl daemon-reload
sudo systemctl enable church-api
sudo systemctl start church-api
# Check if it's running
if sudo systemctl is-active --quiet church-api; then
echo "✅ Church API is running on port $SERVICE_PORT!"
else
echo "❌ Service failed to start. Check logs with: sudo journalctl -u church-api -f"
exit 1
fi
echo ""
echo "🎉 BASIC CHURCH API DEPLOYED SUCCESSFULLY! 🎉"
echo ""
echo "Next steps:"
echo "1. Update .env file with your SMTP credentials"
echo "2. Add api.rockvilletollandsda.church to your Caddy config"
echo "3. Implement the full handlers (or let me know if you want the complete code)"
echo "4. Test with: curl http://localhost:$SERVICE_PORT/api/auth/login"
echo ""
echo "Default admin login:"
echo " Username: admin"
echo " Password: admin123"
echo ""
echo "🗑️ Ready to destroy PocketBase once everything works!"
EOF

View file

@ -1,18 +0,0 @@
[package]
name = "axum-church-website"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["fs", "cors", "compression-gzip"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
dotenvy = "0.15"

View file

@ -1,688 +0,0 @@
/* 2025 Modern Church Website Design */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&family=Inter:wght@300;400;500;600;700;800&display=swap');
:root {
--midnight: #0a0a0f;
--deep-navy: #1a1a2e;
--royal-blue: #16213e;
--soft-gold: #d4af37;
--warm-gold: #f7d794;
--pearl-white: #fefefe;
--soft-gray: #f5f6fa;
--medium-gray: #57606f;
--charcoal: #2f3542;
--gradient-primary: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
--gradient-gold: linear-gradient(135deg, #d4af37 0%, #f7d794 100%);
--gradient-glass: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
--shadow-soft: 0 4px 20px rgba(26, 26, 46, 0.08);
--shadow-medium: 0 8px 40px rgba(26, 26, 46, 0.12);
--shadow-strong: 0 20px 60px rgba(26, 26, 46, 0.15);
--shadow-glow: 0 0 40px rgba(212, 175, 55, 0.3);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
overflow-x: hidden;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.6;
color: var(--charcoal);
background: var(--pearl-white);
overflow-x: hidden;
padding-top: 80px;
}
/* Typography System */
.serif { font-family: 'Playfair Display', serif; }
.sans { font-family: 'Inter', sans-serif; }
h1, .h1 { font-size: clamp(2.5rem, 6vw, 5rem); font-weight: 800; }
h2, .h2 { font-size: clamp(2rem, 4vw, 3.5rem); font-weight: 700; }
h3, .h3 { font-size: clamp(1.5rem, 3vw, 2.5rem); font-weight: 600; }
h4, .h4 { font-size: clamp(1.2rem, 2.5vw, 2rem); font-weight: 600; }
.text-lg { font-size: clamp(1.1rem, 2vw, 1.3rem); }
.text-xl { font-size: clamp(1.3rem, 2.5vw, 1.6rem); }
.text-2xl { font-size: clamp(1.6rem, 3vw, 2rem); }
/* Advanced Navigation */
.nav-2025 {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(10, 10, 15, 0.95);
backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid rgba(212, 175, 55, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 80px;
}
.nav-container {
max-width: 1600px;
margin: 0 auto;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-brand {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
font-weight: 700;
color: var(--soft-gold);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.75rem;
}
.nav-menu {
display: flex;
list-style: none;
gap: 1.5rem;
align-items: center;
margin: 0;
}
.nav-link {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
font-weight: 500;
font-size: 0.95rem;
padding: 0.75rem 1.25rem;
border-radius: 8px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
height: 40px;
}
.nav-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: var(--gradient-gold);
transition: left 0.3s ease;
z-index: -1;
}
.nav-link:hover::before {
left: 0;
}
.nav-link:hover {
color: var(--midnight);
transform: translateY(-1px);
}
/* Hero Section 2025 */
.hero-2025 {
min-height: 100vh;
background: var(--gradient-primary);
position: relative;
display: flex;
align-items: center;
overflow: hidden;
}
.hero-2025::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 50%, rgba(212, 175, 55, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(212, 175, 55, 0.15) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(212, 175, 55, 0.1) 0%, transparent 50%);
}
.hero-content {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
position: relative;
z-index: 2;
}
.hero-text {
color: white;
}
.hero-title {
font-family: 'Playfair Display', serif;
font-size: clamp(3rem, 6vw, 5.5rem);
font-weight: 800;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, var(--warm-gold) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: clamp(1.2rem, 2.5vw, 1.5rem);
font-weight: 400;
margin-bottom: 2rem;
opacity: 0.9;
line-height: 1.6;
}
.hero-cta-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 3rem;
}
.btn-2025 {
padding: 1rem 2rem;
border-radius: 12px;
border: none;
font-weight: 600;
font-size: 1rem;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
}
.btn-primary {
background: var(--gradient-gold);
color: var(--midnight);
box-shadow: var(--shadow-medium);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-strong), var(--shadow-glow);
color: var(--midnight);
}
.btn-outline {
background: transparent;
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
}
.btn-outline:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--soft-gold);
color: white;
}
.hero-visual {
position: relative;
height: 600px;
}
.floating-card {
position: absolute;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 2rem;
box-shadow: var(--shadow-medium);
animation: float 6s ease-in-out infinite;
}
.floating-card:nth-child(1) {
top: 10%;
right: 10%;
animation-delay: 0s;
}
.floating-card:nth-child(2) {
top: 50%;
right: 30%;
animation-delay: 2s;
}
.floating-card:nth-child(3) {
bottom: 20%;
right: 5%;
animation-delay: 4s;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(2deg); }
}
/* Modern Section Layouts */
.section-2025 {
padding: 6rem 0;
position: relative;
}
.container-2025 {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
}
.section-header {
text-align: center;
margin-bottom: 4rem;
}
.section-title {
font-family: 'Playfair Display', serif;
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 700;
color: var(--deep-navy);
margin-bottom: 1rem;
}
.section-subtitle {
font-size: clamp(1.1rem, 2vw, 1.3rem);
color: var(--medium-gray);
max-width: 600px;
margin: 0 auto;
font-weight: 400;
}
/* Premium Cards System */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-top: 4rem;
}
.card-2025 {
background: white;
border-radius: 24px;
padding: 3rem;
box-shadow: var(--shadow-soft);
border: 1px solid rgba(26, 26, 46, 0.05);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.card-2025::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--gradient-gold);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.card-2025:hover::before {
transform: scaleX(1);
}
.card-2025:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-strong);
}
.card-icon-2025 {
width: 70px;
height: 70px;
background: var(--gradient-gold);
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
font-size: 1.8rem;
color: var(--midnight);
box-shadow: var(--shadow-soft);
}
.card-title {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--deep-navy);
margin-bottom: 1rem;
}
.card-text {
color: var(--medium-gray);
line-height: 1.7;
font-size: 1rem;
}
/* Three Angels Section */
.angels-2025 {
background: var(--soft-gray);
position: relative;
overflow: hidden;
}
.angels-2025::before {
content: '';
position: absolute;
top: -50px;
left: 0;
right: 0;
height: 100px;
background: var(--pearl-white);
transform: skewY(-2deg);
}
.angels-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 4rem;
}
/* Service Times Modern Layout */
.services-2025 {
background: var(--deep-navy);
color: white;
position: relative;
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
margin-top: 4rem;
}
.service-card-2025 {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 2.5rem;
text-align: center;
transition: all 0.3s ease;
}
.service-card-2025:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-5px);
}
.service-icon-2025 {
width: 80px;
height: 80px;
background: var(--gradient-gold);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
font-size: 2rem;
color: var(--midnight);
}
.service-time {
font-family: 'Playfair Display', serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--soft-gold);
margin: 1rem 0;
}
/* Events Section */
.events-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
margin-top: 4rem;
}
.event-card-2025 {
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: var(--shadow-soft);
transition: all 0.3s ease;
}
.event-card-2025:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-medium);
}
.event-image-2025 {
height: 200px;
background: var(--gradient-primary);
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200"><defs><linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:%23d4af37;stop-opacity:0.3" /><stop offset="100%" style="stop-color:%23f7d794;stop-opacity:0.1" /></linearGradient></defs><circle cx="100" cy="50" r="30" fill="url(%23grad)" opacity="0.3"/><circle cx="300" cy="150" r="20" fill="url(%23grad)" opacity="0.2"/><circle cx="200" cy="100" r="40" fill="url(%23grad)" opacity="0.15"/></svg>');
background-size: cover;
background-position: center;
}
.event-date-badge {
position: absolute;
top: 1rem;
right: 1rem;
background: var(--soft-gold);
color: var(--midnight);
padding: 0.5rem 1rem;
border-radius: 12px;
font-weight: 600;
font-size: 0.9rem;
}
.event-content-2025 {
padding: 2rem;
}
.event-title {
font-family: 'Playfair Display', serif;
font-size: 1.3rem;
font-weight: 600;
color: var(--deep-navy);
margin-bottom: 1rem;
}
/* Footer 2025 */
.footer-2025 {
background: var(--midnight);
color: white;
padding: 4rem 0 2rem;
position: relative;
}
.footer-2025::before {
content: '';
position: absolute;
top: -50px;
left: 0;
right: 0;
height: 100px;
background: var(--pearl-white);
transform: skewY(-2deg);
}
/* Mobile Navigation */
.nav-toggle {
display: none;
flex-direction: column;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
}
.hamburger {
width: 24px;
height: 2px;
background: var(--soft-gold);
margin: 2px 0;
transition: all 0.3s ease;
border-radius: 2px;
}
/* Responsive Design */
@media (max-width: 992px) {
.nav-toggle {
display: flex;
}
.nav-menu {
position: fixed;
top: 100%;
left: 0;
right: 0;
background: rgba(10, 10, 15, 0.98);
backdrop-filter: blur(20px);
flex-direction: column;
padding: 2rem;
gap: 1.5rem;
transform: translateY(-100vh);
transition: transform 0.3s ease;
border-top: 1px solid rgba(212, 175, 55, 0.2);
}
.nav-menu.active {
transform: translateY(0);
}
.nav-toggle.active .hamburger:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.nav-toggle.active .hamburger:nth-child(2) {
opacity: 0;
}
.nav-toggle.active .hamburger:nth-child(3) {
transform: rotate(-45deg) translate(7px, -6px);
}
.hero-content {
grid-template-columns: 1fr;
text-align: center;
}
.hero-visual {
height: 400px;
}
.cards-grid,
.angels-grid,
.services-grid,
.events-grid {
grid-template-columns: 1fr;
}
.section-2025 {
padding: 4rem 0;
}
}
/* Optimized Scroll Animations for Performance */
.scroll-reveal {
opacity: 0;
transform: translate3d(0, 20px, 0);
will-change: opacity, transform;
transition: opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.scroll-reveal.revealed {
opacity: 1;
transform: translate3d(0, 0, 0);
will-change: auto;
}
/* Reduced stagger delays for smoother performance */
.stagger-1 { transition-delay: 0.05s; }
.stagger-2 { transition-delay: 0.1s; }
.stagger-3 { transition-delay: 0.15s; }
.stagger-4 { transition-delay: 0.2s; }
/* Performance optimization for animations */
@media (prefers-reduced-motion: reduce) {
.scroll-reveal {
opacity: 1;
transform: none;
transition: none;
}
}
/* GPU acceleration for smooth animations */
.card-2025, .event-card-2025, .floating-card {
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
/* Optimize hover animations */
.card-2025:hover, .event-card-2025:hover {
transform: translateZ(0) translateY(-5px);
transition: transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* Disable heavy animations on lower-end devices */
@media (max-width: 992px), (prefers-reduced-motion: reduce) {
.floating-card {
animation: none !important;
}
.hero-visual::before {
animation: none !important;
}
}
/* Optimize floating animation */
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg) translateZ(0);
}
50% {
transform: translateY(-15px) rotate(1deg) translateZ(0);
}
}
/* Reduce animation complexity on mobile */
@media (max-width: 992px) {
.btn-2025:hover {
transform: none;
}
.nav-link:hover {
transform: none;
}
}

View file

@ -1,51 +0,0 @@
#!/bin/bash
# RTSDA Church Website Deployment Script
set -e
echo "Setting up RTSDA Church Website..."
# Create directories
sudo mkdir -p /var/www/rtsda-website
sudo mkdir -p /opt/rtsda-website
# Copy static assets to web directory
sudo cp -r css/ js/ images/ /var/www/rtsda-website/
sudo chown -R www-data:www-data /var/www/rtsda-website
# Copy source code to opt directory
sudo cp -r src/ Cargo.toml Cargo.lock /opt/rtsda-website/
sudo chown -R rockvilleav:rockvilleav /opt/rtsda-website
# Build the application
cd /opt/rtsda-website
cargo build --release
# Create systemd service file
sudo tee /etc/systemd/system/rtsda-website.service > /dev/null <<EOF
[Unit]
Description=RTSDA Church Website
After=network.target
[Service]
Type=simple
User=rockvilleav
Group=rockvilleav
WorkingDirectory=/opt/rtsda-website
ExecStart=/opt/rtsda-website/target/release/axum-church-website
Restart=always
RestartSec=10
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
EOF
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable rtsda-website
sudo systemctl start rtsda-website
echo "Deployment complete!"
echo "Service status:"
sudo systemctl status rtsda-website --no-pager

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

View file

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download on the App Store</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269C110.85987,0,110.49457,0,110.13477,0Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875h102.769l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993A5.76267,5.76267,0,0,1,118.8672,5.667a12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.5459,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238h1.79883v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C63.60984,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238h1.31641V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g>
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.13381,2.13381,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 135 40" fill="none">
<path d="M130 40H5c-2.8 0-5-2.2-5-5V5c0-2.8 2.2-5 5-5h125c2.8 0 5 2.2 5 5v30c0 2.8-2.2 5-5 5z"/>
<path d="M130 .8c2.3 0 4.2 1.9 4.2 4.2v30c0 2.3-1.9 4.2-4.2 4.2H5C2.7 39.2.8 37.3.8 35V5C.8 2.7 2.7.8 5 .8h125m0-.8H5C2.2 0 0 2.2 0 5v30c0 2.8 2.2 5 5 5h125c2.8 0 5-2.2 5-5V5c0-2.8-2.2-5-5-5z" fill="#A6A6A6"/>
<path d="M47.4 10.2c0 .8-.2 1.5-.7 2-.5.6-1.2.8-2.1.8-.8 0-1.6-.3-2.1-.8-.6-.6-.9-1.3-.9-2.2 0-.9.3-1.6.9-2.2.6-.6 1.3-.8 2.1-.8.4 0 .8.1 1.2.3.4.2.7.4.9.7l-.5.5c-.4-.5-.9-.7-1.6-.7-.6 0-1.2.2-1.6.7-.5.4-.7 1-.7 1.7s.2 1.3.7 1.7c.5.4 1 .7 1.6.7.7 0 1.2-.2 1.7-.7.3-.3.5-.7.5-1.2h-2.2v-.8h2.9v.4zM52 7.7h-2.7v1.9h2.5v.7h-2.5v1.9H52v.8h-3.5V7H52v.7zM55.3 13h-.8V7.7h-1.7V7H57v.7h-1.7V13zM59.9 13V7h.8v6h-.8zM64.1 13h-.8V7.7h-1.7V7h4.1v.7H64V13zM73.6 12.2c-.6.6-1.3.9-2.2.9-.9 0-1.6-.3-2.2-.9-.6-.6-.9-1.3-.9-2.2s.3-1.6.9-2.2c.6-.6 1.3-.9 2.2-.9.9 0 1.6.3 2.2.9.6.6.9 1.3.9 2.2 0 .9-.3 1.6-.9 2.2zm-3.8-.5c.4.4 1 .7 1.6.7.6 0 1.2-.2 1.6-.7.4-.4.7-1 .7-1.7s-.2-1.3-.7-1.7c-.4-.4-1-.7-1.6-.7-.6 0-1.2.2-1.6.7-.4.4-.7 1-.7 1.7s.2 1.3.7 1.7zM75.6 13V7h.9l2.9 4.7V7h.8v6h-.8l-3.1-4.9V13h-.7z" fill="#fff" stroke="#fff" stroke-width=".2" stroke-miterlimit="10"/>
<path d="M68.1 21.8c-2.4 0-4.3 1.8-4.3 4.3 0 2.4 1.9 4.3 4.3 4.3s4.3-1.8 4.3-4.3c0-2.6-1.9-4.3-4.3-4.3zm0 6.8c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6c1.3 0 2.4 1 2.4 2.6 0 1.5-1.1 2.6-2.4 2.6zm-9.3-6.8c-2.4 0-4.3 1.8-4.3 4.3 0 2.4 1.9 4.3 4.3 4.3s4.3-1.8 4.3-4.3c0-2.6-1.9-4.3-4.3-4.3zm0 6.8c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6c1.3 0 2.4 1 2.4 2.6 0 1.5-1.1 2.6-2.4 2.6zm-11.1-5.5v1.8H52c-.1 1-.5 1.8-1 2.3-.6.6-1.6 1.3-3.3 1.3-2.7 0-4.7-2.1-4.7-4.8s2.1-4.8 4.7-4.8c1.4 0 2.5.6 3.3 1.3l1.3-1.3c-1.1-1-2.5-1.8-4.5-1.8-3.6 0-6.7 3-6.7 6.6 0 3.6 3.1 6.6 6.7 6.6 2 0 3.4-.6 4.6-1.9 1.2-1.2 1.6-2.9 1.6-4.2 0-.4 0-.8-.1-1.1h-6.2zm45.4 1.4c-.4-1-1.4-2.7-3.6-2.7s-4 1.7-4 4.3c0 2.4 1.8 4.3 4.2 4.3 1.9 0 3.1-1.2 3.5-1.9l-1.4-1c-.5.7-1.1 1.2-2.1 1.2s-1.6-.4-2.1-1.3l5.7-2.4-.2-.5zm-5.8 1.4c0-1.6 1.3-2.5 2.2-2.5.7 0 1.4.4 1.6.9l-3.8 1.6zM82.6 30h1.9V17.5h-1.9V30zm-3-7.3c-.5-.5-1.3-1-2.3-1-2.1 0-4.1 1.9-4.1 4.3s1.9 4.3 4.1 4.3c1 0 1.8-.5 2.2-1h.1v.6c0 1.6-.9 2.5-2.3 2.5-1.1 0-1.9-.8-2.1-1.5l-1.6.7c.5 1.1 1.7 2.5 3.8 2.5 2.2 0 4-1.3 4-4.4V22h-1.8v.7zm-2.2 5.9c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6c1.3 0 2.3 1.1 2.3 2.6s-1 2.6-2.3 2.6zm24.4-11.1h-4.5V30h1.9v-4.7h2.6c2.1 0 4.1-1.5 4.1-3.9s-2-3.9-4.1-3.9zm.1 6h-2.7v-4.3h2.7c1.4 0 2.2 1.2 2.2 2.1-.1 1.1-.9 2.2-2.2 2.2zm11.5-1.8c-1.4 0-2.8.6-3.3 1.9l1.7.7c.4-.7 1-.9 1.7-.9 1 0 1.9.6 2 1.6v.1c-.3-.2-1.1-.5-1.9-.5-1.8 0-3.6 1-3.6 2.8 0 1.7 1.5 2.8 3.1 2.8 1.3 0 1.9-.6 2.4-1.2h.1v1h1.8v-4.8c-.2-2.2-1.9-3.5-4-3.5zm-.2 6.9c-.6 0-1.5-.3-1.5-1.1 0-1 1.1-1.3 2-1.3.8 0 1.2.2 1.7.4-.2 1.2-1.2 2-2.2 2zm10.5-6.6l-2.1 5.4h-.1l-2.2-5.4h-2l3.3 7.6-1.9 4.2h1.9l5.1-11.8h-2zm-16.8 8h1.9V17.5h-1.9V30z" fill="#fff"/>
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="21.8" y1="33.29" x2="5.02" y2="16.51" gradientTransform="matrix(1 0 0 -1 0 42)">
<stop offset="0" stop-color="#00A0FF"/>
<stop offset=".01" stop-color="#00A1FF"/>
<stop offset=".26" stop-color="#00BEFF"/>
<stop offset=".51" stop-color="#00D2FF"/>
<stop offset=".76" stop-color="#00DFFF"/>
<stop offset="1" stop-color="#00E3FF"/>
</linearGradient>
<path d="M10.4 7.5c-.3.3-.4.8-.4 1.4V31c0 .6.2 1.1.5 1.4l.1.1L23 20.1v-.2L10.4 7.5z" fill="url(#a)"/>
<linearGradient id="b" gradientUnits="userSpaceOnUse" x1="33.83" y1="21.999" x2="9.64" y2="21.999" gradientTransform="matrix(1 0 0 -1 0 42)">
<stop offset="0" stop-color="#FFE000"/>
<stop offset=".41" stop-color="#FFBD00"/>
<stop offset=".78" stop-color="orange"/>
<stop offset="1" stop-color="#FF9C00"/>
</linearGradient>
<path d="M27 24.3l-4-4L23 20v.1l4 4-.1.1 4.9 2.8c1.4.8 1.4 2.1 0 2.9l-5-2.8" fill="url(#b)"/>
<linearGradient id="c" gradientUnits="userSpaceOnUse" x1="24.83" y1="19.704" x2="2.07" y2="-3.086" gradientTransform="matrix(1 0 0 -1 0 42)">
<stop offset="0" stop-color="#FF3A44"/>
<stop offset="1" stop-color="#C31162"/>
</linearGradient>
<path d="M27.1 24.2L22.9 20 10.4 32.5c.5.5 1.2.5 2.1.1l14.6-8.4" fill="url(#c)"/>
<linearGradient id="d" gradientUnits="userSpaceOnUse" x1="7.297" y1="41.824" x2="17.46" y2="31.661" gradientTransform="matrix(1 0 0 -1 0 42)">
<stop offset="0" stop-color="#32A071"/>
<stop offset=".07" stop-color="#2DA771"/>
<stop offset=".48" stop-color="#15CF74"/>
<stop offset=".8" stop-color="#06E775"/>
<stop offset="1" stop-color="#00F076"/>
</linearGradient>
<path d="M27.1 15.8L12.5 7.5c-.9-.5-1.6-.4-2.1.1L22.9 20l4.2-4.2z" fill="url(#d)"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -1,84 +0,0 @@
// Contact form submission
document.addEventListener('DOMContentLoaded', function() {
const contactForm = document.getElementById('contact-form');
if (contactForm) {
contactForm.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(contactForm);
const formMessage = document.getElementById('form-message');
try {
const response = await fetch('/contact', {
method: 'POST',
body: formData
});
const result = await response.json();
formMessage.style.display = 'block';
if (result.success) {
formMessage.style.background = '#d4edda';
formMessage.style.color = '#155724';
formMessage.textContent = result.message;
contactForm.reset();
} else {
formMessage.style.background = '#f8d7da';
formMessage.style.color = '#721c24';
formMessage.textContent = result.message;
}
} catch (error) {
formMessage.style.display = 'block';
formMessage.style.background = '#f8d7da';
formMessage.style.color = '#721c24';
formMessage.textContent = 'An error occurred. Please try again later.';
}
});
}
});
// Add smooth scrolling for anchor links
document.addEventListener('DOMContentLoaded', function() {
const links = document.querySelectorAll('a[href^="#"]');
links.forEach(link => {
link.addEventListener('click', function(e) {
const href = this.getAttribute('href');
if (href !== '#') {
e.preventDefault();
const target = document.querySelector(href);
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
});
});
});
// Load random Bible verse on homepage (if not already loaded)
async function loadRandomBibleVerse() {
const verseContainer = document.getElementById('random-verse');
if (verseContainer && !verseContainer.dataset.loaded) {
try {
const response = await fetch('https://api.rockvilletollandsda.church/api/bible_verses/random');
const result = await response.json();
if (result.success && result.data) {
verseContainer.innerHTML = `
<blockquote>"${result.data.text}"</blockquote>
<cite>- ${result.data.reference}</cite>
`;
verseContainer.dataset.loaded = 'true';
}
} catch (error) {
console.error('Error loading Bible verse:', error);
}
}
}
// Load verse on page load if container exists
document.addEventListener('DOMContentLoaded', loadRandomBibleVerse);

View file

@ -1,160 +0,0 @@
use axum::response::Html;
use crate::layout::layout;
pub async fn about_handler() -> Html<String> {
let content = r#"
<!-- About Page -->
<section class="section-2025" style="padding-top: 6rem;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="font-size: 3rem; margin-bottom: 2rem;">About Our Church</h1>
<div style="text-align: center; margin-bottom: 3rem;">
<img src="/images/sda-logo.webp" alt="Seventh-day Adventist Church Logo" style="height: 8rem; width: auto;">
</div>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 1000px; margin: 0 auto;">
<div style="padding: 3rem;">
<p style="font-size: 1.25rem; line-height: 1.8; margin-bottom: 2rem; color: var(--deep-navy);">
Welcome to the <strong>Rockville-Tolland Seventh-day Adventist Church</strong>. We are a vibrant community
of believers dedicated to sharing God's love and the hope of His soon return. Our church
family is committed to spiritual growth, fellowship, and service to our local community.
</p>
<h2 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2.5rem; margin: 3rem 0 1.5rem 0; border-bottom: 3px solid var(--soft-gold); padding-bottom: 0.5rem; display: inline-block;">Our Mission</h2>
<p style="font-size: 1.1rem; line-height: 1.7; margin-bottom: 2rem; color: var(--deep-navy);">
Our mission is to share the everlasting gospel of Jesus Christ in the context of the <strong>Three
Angels' Messages of Revelation 14</strong>, leading people to accept Jesus as their personal Savior
and unite with His remnant church in preparation for His soon return.
</p>
<h2 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2.5rem; margin: 3rem 0 1.5rem 0; border-bottom: 3px solid var(--soft-gold); padding-bottom: 0.5rem; display: inline-block;">What We Believe</h2>
<p style="font-size: 1.1rem; line-height: 1.7; margin-bottom: 1.5rem; color: var(--deep-navy);">
As Seventh-day Adventists, we believe in:
</p>
<div class="beliefs-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin: 2rem 0;">
<div class="belief-card" style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; border-left: 4px solid var(--soft-gold);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<i class="fas fa-book" style="color: var(--soft-gold); font-size: 1.5rem;"></i>
<h4 style="color: var(--deep-navy); font-weight: 600; margin: 0;">The Bible</h4>
</div>
<p style="color: var(--deep-navy); margin: 0;">The inspired Word of God and our only rule of faith and practice</p>
</div>
<div class="belief-card" style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; border-left: 4px solid var(--soft-gold);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<i class="fas fa-cross" style="color: var(--soft-gold); font-size: 1.5rem;"></i>
<h4 style="color: var(--deep-navy); font-weight: 600; margin: 0;">Salvation</h4>
</div>
<p style="color: var(--deep-navy); margin: 0;">Through faith in Jesus Christ alone, by grace, not by works</p>
</div>
<div class="belief-card" style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; border-left: 4px solid var(--soft-gold);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<i class="fas fa-cloud" style="color: var(--soft-gold); font-size: 1.5rem;"></i>
<h4 style="color: var(--deep-navy); font-weight: 600; margin: 0;">Second Coming</h4>
</div>
<p style="color: var(--deep-navy); margin: 0;">The blessed hope and grand climax of the gospel</p>
</div>
<div class="belief-card" style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; border-left: 4px solid var(--soft-gold);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<i class="fas fa-calendar-week" style="color: var(--soft-gold); font-size: 1.5rem;"></i>
<h4 style="color: var(--deep-navy); font-weight: 600; margin: 0;">The Sabbath</h4>
</div>
<p style="color: var(--deep-navy); margin: 0;">God's holy day of rest and worship from Friday sunset to Saturday sunset</p>
</div>
<div class="belief-card" style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; border-left: 4px solid var(--soft-gold);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<i class="fas fa-heart" style="color: var(--soft-gold); font-size: 1.5rem;"></i>
<h4 style="color: var(--deep-navy); font-weight: 600; margin: 0;">Wholistic Health</h4>
</div>
<p style="color: var(--deep-navy); margin: 0;">Caring for body, mind, and spirit as God's temple</p>
</div>
<div class="belief-card" style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; border-left: 4px solid var(--soft-gold);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<i class="fas fa-hands-helping" style="color: var(--soft-gold); font-size: 1.5rem;"></i>
<h4 style="color: var(--deep-navy); font-weight: 600; margin: 0;">Service</h4>
</div>
<p style="color: var(--deep-navy); margin: 0;">To God and humanity, following Christ's example</p>
</div>
</div>
<h2 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2.5rem; margin: 3rem 0 1.5rem 0; border-bottom: 3px solid var(--soft-gold); padding-bottom: 0.5rem; display: inline-block;">Our Community</h2>
<p style="font-size: 1.1rem; line-height: 1.7; margin-bottom: 2rem; color: var(--deep-navy);">
We are blessed to serve the communities of <strong>Rockville, Tolland, and surrounding areas</strong>. Our
church offers various programs and ministries for all age groups, providing opportunities
for worship, fellowship, and spiritual growth.
</p>
<h2 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2.5rem; margin: 3rem 0 1.5rem 0; border-bottom: 3px solid var(--soft-gold); padding-bottom: 0.5rem; display: inline-block;">The Three Angels' Messages</h2>
<p style="font-size: 1.1rem; line-height: 1.7; margin-bottom: 2rem; color: var(--deep-navy);">
Central to our identity as Seventh-day Adventists are the Three Angels' Messages found in Revelation 14:6-12:
</p>
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold);">
<h4 style="color: var(--deep-navy); font-weight: 600; margin-bottom: 1rem;">
<i class="fas fa-dove" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
First Angel's Message
</h4>
<p style="font-style: italic; color: var(--deep-navy); margin-bottom: 1rem;">
"Fear God and give glory to Him, for the hour of His judgment has come; and worship Him who made heaven and earth, the sea and springs of water."
</p>
<p style="color: var(--deep-navy); margin: 0; font-size: 0.95rem;">
The everlasting gospel calls all people to worship the Creator God.
</p>
</div>
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold);">
<h4 style="color: var(--deep-navy); font-weight: 600; margin-bottom: 1rem;">
<i class="fas fa-bullhorn" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
Second Angel's Message
</h4>
<p style="font-style: italic; color: var(--deep-navy); margin-bottom: 1rem;">
"Babylon is fallen, is fallen, that great city, because she has made all nations drink of the wine of the wrath of her fornication."
</p>
<p style="color: var(--deep-navy); margin: 0; font-size: 0.95rem;">
A warning about false religious systems and a call to choose truth over tradition.
</p>
</div>
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold);">
<h4 style="color: var(--deep-navy); font-weight: 600; margin-bottom: 1rem;">
<i class="fas fa-scroll" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
Third Angel's Message
</h4>
<p style="font-style: italic; color: var(--deep-navy); margin-bottom: 1rem;">
"Here is the patience of the saints; here are those who keep the commandments of God and the faith of Jesus."
</p>
<p style="color: var(--deep-navy); margin: 0; font-size: 0.95rem;">
A call to remain faithful to God's commandments, including the Sabbath, while maintaining faith in Jesus.
</p>
</div>
<h2 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2.5rem; margin: 3rem 0 1.5rem 0; border-bottom: 3px solid var(--soft-gold); padding-bottom: 0.5rem; display: inline-block;">Join Us</h2>
<p style="font-size: 1.1rem; line-height: 1.7; margin-bottom: 2rem; color: var(--deep-navy);">
Whether you're a long-time Adventist, new to the faith, or simply seeking to learn more
about God, we welcome you to join us. Our services are designed to be inclusive and
meaningful for everyone, regardless of where you are in your spiritual journey.
</p>
<div style="text-align: center; margin-top: 3rem;">
<a href="/contact" class="btn-2025 btn-primary" style="font-size: 1.1rem; padding: 1rem 2rem;">
<i class="fas fa-envelope" style="margin-right: 0.5rem;"></i>
Contact Us
</a>
<a href="/events" class="btn-2025 btn-outline" style="font-size: 1.1rem; padding: 1rem 2rem; margin-left: 1rem; background: white; color: var(--deep-navy); border: 2px solid var(--deep-navy);">
<i class="fas fa-calendar" style="margin-right: 0.5rem;"></i>
Upcoming Events
</a>
</div>
</div>
</div>
</div>
</section>
"#;
Html(layout(content, "About Our Church"))
}

View file

@ -1,378 +0,0 @@
use axum::{extract::Path, response::Html};
use crate::services::ApiService;
use crate::layout::layout;
use chrono::NaiveDate;
pub async fn bulletins_handler() -> Html<String> {
let api_service = ApiService::new();
match api_service.get_bulletins().await {
Ok(bulletins) => {
let content = format!(r#"
<!-- Bulletins Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Church Bulletins</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Download our weekly bulletins to stay informed about church activities and worship services</p>
</div>
</div>
</section>
<!-- Bulletins Grid -->
<section class="section-2025">
<div class="container-2025">
<div class="cards-grid">
{}
</div>
{}
</div>
</section>
"#,
// Bulletins grid HTML
bulletins.iter().enumerate().map(|(index, bulletin)| {
let formatted_date = if let Ok(parsed_date) = NaiveDate::parse_from_str(&bulletin.date, "%Y-%m-%d") {
parsed_date.format("%A, %B %d, %Y").to_string()
} else {
bulletin.date.clone()
};
format!(r#"
<a href="/bulletins/{}" style="text-decoration: none; color: inherit; display: block;">
<div class="card-2025 scroll-reveal stagger-{}" style="cursor: pointer;">
<div class="card-icon-2025">
<i class="fas fa-file-alt"></i>
</div>
<h3 class="card-title">{}</h3>
<p style="color: var(--medium-gray); margin-bottom: 1.5rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
{}
<div class="btn-2025 btn-outline" style="display: inline-flex; align-items: center; gap: 0.5rem; margin-top: auto; background: white; color: var(--deep-navy); border: 2px solid var(--deep-navy);">
View Details <i class="fas fa-arrow-right"></i>
</div>
</div>
</a>
"#,
bulletin.id,
(index % 3) + 1,
bulletin.title,
formatted_date,
// Scripture reading preview
if let Some(ref scripture) = bulletin.scripture_reading {
if !scripture.is_empty() {
let preview = if scripture.len() > 150 {
format!("{}...", &scripture[..150])
} else {
scripture.clone()
};
format!(r#"
<div style="background: var(--soft-gray); padding: 1.5rem; border-radius: 12px; margin: 1.5rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy);">
<strong style="color: var(--deep-navy);">Scripture Reading:</strong><br>
{}
</div>
"#, preview)
} else {
String::new()
}
} else {
String::new()
}
)
}).collect::<Vec<_>>().join(""),
// No bulletins message
if bulletins.is_empty() {
r#"
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-file-alt"></i>
</div>
<h3 class="card-title">No Bulletins Available</h3>
<p class="card-text">No bulletins available at this time. Please check back later.</p>
</div>
"#
} else {
""
}
);
Html(layout(&content, "Bulletins"))
},
Err(_) => {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Bulletins</h1>
<p>Unable to load bulletins. Please try again later.</p>
</div>
</div>
</section>
"#;
Html(layout(content, "Bulletins"))
}
}
}
pub async fn bulletin_detail_handler(Path(id): Path<String>) -> Html<String> {
let api_service = ApiService::new();
match api_service.get_bulletin(&id).await {
Ok(Some(bulletin)) => {
let formatted_date = if let Ok(parsed_date) = NaiveDate::parse_from_str(&bulletin.date, "%Y-%m-%d") {
parsed_date.format("%A, %B %d, %Y").to_string()
} else {
bulletin.date.clone()
};
let content = format!(r#"
<!-- Bulletin Detail Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 3rem 0;">
<div class="container-2025">
<a href="/bulletins" style="color: rgba(255,255,255,0.8); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500;">
<i class="fas fa-arrow-left"></i> Back to Bulletins
</a>
<h1 class="section-title serif" style="color: white; font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">{}</h1>
<p style="color: rgba(255,255,255,0.9); font-size: 1.2rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
</div>
</section>
{}
{}
{}
{}
"#,
bulletin.title,
formatted_date,
// Scripture Reading Section
if let Some(ref scripture) = bulletin.scripture_reading {
if !scripture.is_empty() {
format!(r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 800px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 class="card-title">Scripture Reading</h3>
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy); line-height: 1.6;">
{}
</div>
</div>
</div>
</section>
"#, scripture)
} else {
String::new()
}
} else {
String::new()
},
// Service Programs Section
if bulletin.sabbath_school.is_some() || bulletin.divine_worship.is_some() {
let has_both = bulletin.sabbath_school.is_some() && bulletin.divine_worship.is_some();
format!(r#"
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Service Programs</h2>
<p class="section-subtitle">Order of service for worship and fellowship</p>
</div>
{}
</div>
</section>
"#,
if has_both {
// Both programs - adaptive grid
format!(r#"
<div style="display: grid; grid-template-columns: 1fr 1.5fr; gap: 3rem; margin-top: 3rem; align-items: start;">
{}
{}
</div>
"#,
// Sabbath School - smaller card
if let Some(ref ss) = bulletin.sabbath_school {
format!(r#"
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2); height: fit-content;">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2.5rem;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--soft-gold), #ffd700); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(255, 215, 120, 0.3);">
<i class="fas fa-book" style="color: var(--deep-navy); font-size: 1.5rem;"></i>
</div>
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.8rem; margin: 0; font-weight: 600;">Sabbath School</h3>
</div>
<div style="color: var(--deep-navy); line-height: 2; font-size: 1.05rem;">
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit;">{}</div>
</div>
</div>
"#, ss)
} else {
String::new()
},
// Divine Worship - larger card
if let Some(ref dw) = bulletin.divine_worship {
format!(r#"
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3.5rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 3rem;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--deep-navy), #2c4584); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(25, 35, 66, 0.3);">
<i class="fas fa-praying-hands" style="color: white; font-size: 1.5rem;"></i>
</div>
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.8rem; margin: 0; font-weight: 600;">Divine Worship</h3>
</div>
<div style="color: var(--deep-navy); line-height: 2.2; font-size: 1.05rem; letter-spacing: 0.3px; overflow: visible; word-wrap: break-word;">
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit; letter-spacing: inherit; overflow: visible; word-wrap: break-word;">{}</div>
</div>
</div>
"#, dw)
} else {
String::new()
}
)
} else {
// Single program or responsive fallback
format!(r#"
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 3rem; margin-top: 3rem;">
{}
{}
</div>
"#,
if let Some(ref ss) = bulletin.sabbath_school {
format!(r#"
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2.5rem;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--soft-gold), #ffd700); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(255, 215, 120, 0.3);">
<i class="fas fa-book" style="color: var(--deep-navy); font-size: 1.5rem;"></i>
</div>
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2rem; margin: 0; font-weight: 600;">Sabbath School Program</h3>
</div>
<div style="color: var(--deep-navy); line-height: 2; font-size: 1.05rem;">
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit;">{}</div>
</div>
</div>
"#, ss)
} else {
String::new()
},
if let Some(ref dw) = bulletin.divine_worship {
format!(r#"
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3.5rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 3rem;">
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--deep-navy), #2c4584); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(25, 35, 66, 0.3);">
<i class="fas fa-praying-hands" style="color: white; font-size: 1.5rem;"></i>
</div>
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2rem; margin: 0; font-weight: 600;">Divine Worship Program</h3>
</div>
<div style="color: var(--deep-navy); line-height: 2.2; font-size: 1.05rem; letter-spacing: 0.3px; overflow: visible; word-wrap: break-word;">
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit; letter-spacing: inherit; overflow: visible; word-wrap: break-word;">{}</div>
</div>
</div>
"#, dw)
} else {
String::new()
}
)
})
} else {
String::new()
},
// Sunset Information Section
if let Some(ref sunset) = bulletin.sunset {
if !sunset.is_empty() {
format!(r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 600px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-sun"></i>
</div>
<h3 class="card-title">Sabbath Information</h3>
<p style="color: var(--medium-gray); font-style: italic; font-size: 1.1rem;">{}</p>
</div>
</div>
</section>
"#, sunset)
} else {
String::new()
}
} else {
String::new()
},
// PDF Download Section
if let Some(ref pdf_path) = bulletin.pdf_path {
if !pdf_path.is_empty() {
format!(r#"
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 600px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-file-pdf"></i>
</div>
<h3 class="card-title">Download Full Bulletin</h3>
<p class="card-text">Get the complete bulletin with all details and information.</p>
<a href="{}" target="_blank" class="btn-2025 btn-primary" style="margin-top: 1rem;">
<i class="fas fa-download"></i>
Download PDF
</a>
</div>
</div>
</section>
"#, pdf_path)
} else {
String::new()
}
} else {
String::new()
}
);
Html(layout(&content, &bulletin.title))
},
Ok(None) => {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Bulletin Not Found</h1>
<p>The requested bulletin could not be found.</p>
<a href="/bulletins" class="btn-2025 btn-primary"> Back to Bulletins</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Bulletin Not Found"))
},
Err(_) => {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Error</h1>
<p>Unable to load bulletin. Please try again later.</p>
<a href="/bulletins" class="btn-2025 btn-primary"> Back to Bulletins</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Error"))
}
}
}

View file

@ -1,319 +0,0 @@
use axum::{response::Html, http::StatusCode, Json, extract::Host};
use serde_json::json;
use crate::services::ApiService;
use crate::models::ContactForm;
use crate::layout::layout;
pub async fn contact_handler(Host(hostname): Host) -> Html<String> {
let api_service = ApiService::new();
let config = api_service.get_config().await.unwrap_or(None);
let church_name = config.as_ref()
.and_then(|c| c.church_name.as_ref())
.map(|s| s.as_str())
.unwrap_or("Rockville Tolland SDA Church");
let church_address = config.as_ref()
.and_then(|c| c.church_address.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
let po_box = config.as_ref()
.and_then(|c| c.po_box.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
let contact_phone = config.as_ref()
.and_then(|c| c.contact_phone.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
let google_maps_url = config.as_ref()
.and_then(|c| c.google_maps_url.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
// Create dynamic email based on current domain
let contact_email = format!("info@{}", hostname);
let content = format!(r#"
<!-- Contact Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Contact Us</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">We'd love to hear from you! Whether you have questions, prayer requests, or just want to connect, please reach out</p>
</div>
</div>
</section>
<!-- Contact Info & Form -->
<section class="section-2025">
<div class="container-2025">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: start;">
<!-- Church Info -->
<div class="card-2025 scroll-reveal">
<div class="card-icon-2025">
<i class="fas fa-church"></i>
</div>
<h3 class="card-title">Church Information</h3>
<div style="margin: 1.5rem 0;">
<h4 style="color: var(--deep-navy); margin-bottom: 1rem;">{}</h4>
{}
{}
<p style="margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-envelope" style="color: var(--soft-gold);"></i>
<a href="mailto:{}" style="color: var(--soft-gold); text-decoration: none;">{}</a>
</p>
{}
</div>
{}
</div>
<!-- Contact Form -->
<div class="card-2025 scroll-reveal">
<div class="card-icon-2025">
<i class="fas fa-paper-plane"></i>
</div>
<h3 class="card-title">Send Us a Message</h3>
<form id="contact-form" style="margin-top: 1.5rem;" onsubmit="handleContactSubmit(event)">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<div>
<label for="first_name" style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">First Name *</label>
<input type="text" id="first_name" name="first_name" required style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div>
<label for="last_name" style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Last Name</label>
<input type="text" id="last_name" name="last_name" style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
</div>
<div style="margin-bottom: 1rem;">
<label for="email" style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Your Email *</label>
<input type="email" id="email" name="email" required style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1rem;">
<label for="phone" style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Phone Number</label>
<input type="tel" id="phone" name="phone" placeholder="(555) 123-4567" onblur="formatPhoneNumber(this)" oninput="keepDigitsOnly(this)" style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1rem;">
<label for="subject" style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Subject *</label>
<select id="subject" name="subject" required style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
<option value="">Select a subject</option>
<option value="general">General Inquiry</option>
<option value="prayer">Prayer Request</option>
<option value="bible_study">Bible Study Interest</option>
<option value="membership">Membership Information</option>
<option value="pastoral">Pastoral Care</option>
<option value="adventist_youth">Adventist Youth</option>
<option value="other">Other</option>
</select>
</div>
<div style="margin-bottom: 1.5rem;">
<label for="message" style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Message *</label>
<textarea id="message" name="message" rows="6" required style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem; resize: vertical;"></textarea>
</div>
<button type="submit" class="btn-2025 btn-primary" style="width: 100%;">
<i class="fas fa-paper-plane"></i>
Send Message
</button>
<div id="form-message" style="margin-top: 1rem; padding: 1rem; border-radius: 8px; display: none;"></div>
</form>
</div>
</div>
</div>
</section>
<!-- Connect Section -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Connect With Us</h2>
<p class="section-subtitle">Multiple ways to get involved and stay connected</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-handshake"></i>
</div>
<h3 class="card-title">Visit Us</h3>
<p class="card-text">Join us for worship any Sabbath morning. Visitors are always welcome!</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-praying-hands"></i>
</div>
<h3 class="card-title">Prayer Requests</h3>
<p class="card-text">Submit your prayer requests and our prayer team will lift you up in prayer.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-bible"></i>
</div>
<h3 class="card-title">Bible Studies</h3>
<p class="card-text">Interested in learning more about the Bible? We offer free Bible studies.</p>
</div>
</div>
</div>
</section>
<!-- Pastor's Welcome -->
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 800px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-user-tie"></i>
</div>
<h3 class="card-title">Pastor's Welcome</h3>
<p class="card-text" style="font-style: italic; font-size: 1.1rem; line-height: 1.7;">Welcome to Rockville Tolland SDA Church! We are a community of believers dedicated to sharing God's love and the Three Angels' Messages with our community. Whether you're a long-time member or a first-time visitor, we're glad you're here.</p>
<p style="margin-top: 1.5rem; color: var(--soft-gold); font-weight: 600;">- Pastor Joseph Piresson</p>
</div>
</div>
</section>
<!-- Mobile Layout -->
<style>
@media (max-width: 768px) {{
.container-2025 > div[style*="grid-template-columns"] {{
grid-template-columns: 1fr !important;
gap: 2rem !important;
}}
}}
</style>
<script>
function keepDigitsOnly(input) {{
input.value = input.value.replace(/\\D/g, '');
}}
function formatPhoneNumber(input) {{
const value = input.value.replace(/\\D/g, '');
if (value.length === 10) {{
input.value = `(${{value.slice(0, 3)}}) ${{value.slice(3, 6)}}-${{value.slice(6, 10)}}`;
}}
}}
async function handleContactSubmit(event) {{
event.preventDefault();
const formData = new FormData(event.target);
const messageDiv = document.getElementById('form-message');
// Convert FormData to JSON object for the backend API
const jsonData = {{
first_name: formData.get('first_name'),
last_name: formData.get('last_name') || null,
email: formData.get('email'),
phone: formData.get('phone') || null,
subject: formData.get('subject'),
message: formData.get('message')
}};
try {{
const response = await fetch('/contact', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify(jsonData)
}});
if (response.ok) {{
messageDiv.style.display = 'block';
messageDiv.style.background = 'var(--soft-green)';
messageDiv.style.color = 'var(--deep-navy)';
messageDiv.style.border = '2px solid var(--green)';
messageDiv.innerHTML = '<i class="fas fa-check-circle"></i> Thank you for your message! We will get back to you soon.';
event.target.reset();
}} else {{
throw new Error('Server error');
}}
}} catch (error) {{
messageDiv.style.display = 'block';
messageDiv.style.background = 'var(--soft-red)';
messageDiv.style.color = 'var(--deep-navy)';
messageDiv.style.border = '2px solid var(--red)';
messageDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> Sorry, there was an error sending your message. Please try again.';
}}
}}
</script>
"#,
church_name,
// Church address
if !church_address.is_empty() {
format!(r#"
<p style="margin-bottom: 0.75rem; display: flex; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold); width: 20px; flex-shrink: 0; margin-top: 2px;"></i>
<span>{}</span>
</p>
"#, church_address)
} else {
String::new()
},
// PO Box
if !po_box.is_empty() {
format!(r#"
<p style="margin-bottom: 0.75rem; display: flex; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-mailbox" style="color: var(--soft-gold); width: 20px; flex-shrink: 0; margin-top: 2px;"></i>
<span>{}</span>
</p>
"#, po_box)
} else {
String::new()
},
// Contact email (for mailto link)
contact_email,
contact_email,
// Contact phone
if !contact_phone.is_empty() {
format!(r#"
<p style="margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-phone" style="color: var(--soft-gold);"></i>
<a href="tel:{}" style="color: var(--soft-gold); text-decoration: none;">{}</a>
</p>
"#, contact_phone, contact_phone)
} else {
String::new()
},
// Google Maps link
if !google_maps_url.is_empty() {
format!(r#"
<a href="{}" target="_blank" class="btn-2025 btn-primary">
<i class="fas fa-directions"></i>
Get Directions
</a>
"#, google_maps_url)
} else {
String::new()
}
);
Html(layout(&content, "Contact"))
}
pub async fn contact_form_handler(Json(form): Json<ContactForm>) -> Result<Json<serde_json::Value>, StatusCode> {
let api_service = ApiService::new();
match api_service.submit_contact_form(&form).await {
Ok(true) => Ok(Json(json!({
"success": true,
"message": "Thank you for your message! We will get back to you soon."
}))),
Ok(false) => Err(StatusCode::INTERNAL_SERVER_ERROR),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}

View file

@ -1,433 +0,0 @@
use axum::{extract::Path, response::Html};
use crate::services::{ApiService, format_event_datetime, strip_html};
use crate::layout::layout;
pub async fn events_handler() -> Html<String> {
let api_service = ApiService::new();
// Fetch data concurrently for better performance
let (upcoming_res, featured_res) = tokio::join!(
api_service.get_events(None), // Get all upcoming events
api_service.get_events(Some(3)) // Get top 3 for featured
);
let upcoming_events = upcoming_res.unwrap_or_default();
let _featured_events = featured_res.unwrap_or_default();
let content = format!(r#"
<section class="section-2025" style="background: var(--soft-gray); padding-top: 6rem;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif">Upcoming Events</h1>
<p class="section-subtitle">Join us for these special occasions and activities</p>
</div>
<div class="events-grid">
{}
</div>
{}
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 600px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-plus-circle"></i>
</div>
<h3 class="card-title">Submit an Event</h3>
<p class="card-text">Have an event you'd like to share with the church community?</p>
<a href="https://events.rockvilletollandsda.church" target="_blank" class="btn-2025 btn-primary" style="margin-top: 1rem;">
<i class="fas fa-paper-plane"></i>
Submit Event Request
</a>
</div>
</div>
</section>
"#,
// Events grid HTML
upcoming_events.iter().enumerate().map(|(index, event)| {
render_event_card(event, false, index)
}).collect::<Vec<_>>().join(""),
// No events message
if upcoming_events.is_empty() {
r#"
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-calendar-alt"></i>
</div>
<h3 class="card-title">No Events Scheduled</h3>
<p class="card-text">No upcoming events at this time. Please check back later for new events.</p>
</div>
"#
} else {
""
}
);
Html(layout(&content, "Events"))
}
pub async fn upcoming_events_handler() -> Html<String> {
let api_service = ApiService::new();
match api_service.get_events(None).await {
Ok(events) => {
let content = format!(r#"
<section class="section-2025" style="background: var(--soft-gray); padding-top: 6rem;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<a href="/events" style="color: var(--medium-gray); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500;">
<i class="fas fa-arrow-left"></i> Back to Events
</a>
<h1 class="section-title serif">All Upcoming Events</h1>
<p class="section-subtitle">Complete list of all scheduled events and activities</p>
</div>
<div class="events-grid">
{}
</div>
{}
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 600px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-plus-circle"></i>
</div>
<h3 class="card-title">Submit an Event</h3>
<p class="card-text">Have an event you'd like to share with the church community?</p>
<a href="https://events.rockvilletollandsda.church" target="_blank" class="btn-2025 btn-primary" style="margin-top: 1rem;">
<i class="fas fa-paper-plane"></i>
Submit Event Request
</a>
</div>
</div>
</section>
"#,
// Events grid HTML
events.iter().enumerate().map(|(index, event)| {
render_event_card(event, false, index)
}).collect::<Vec<_>>().join(""),
// No events message
if events.is_empty() {
r#"
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-calendar-alt"></i>
</div>
<h3 class="card-title">No Events Scheduled</h3>
<p class="card-text">No upcoming events at this time. Please check back later for new events.</p>
</div>
"#
} else {
""
}
);
Html(layout(&content, "Upcoming Events"))
}
Err(_) => {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Unable to Load Events</h1>
<p>We're having trouble loading the events right now. Please try again later.</p>
<a href="/events" class="btn-2025 btn-primary"> Back to Events</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Upcoming Events"))
}
}
}
pub async fn event_detail_handler(Path(id): Path<String>) -> Html<String> {
let api_service = ApiService::new();
match api_service.get_event(&id).await {
Ok(Some(event)) => {
let content = format!(r#"
<!-- Event Detail Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 3rem 0;">
<div class="container-2025">
<a href="/events" style="color: rgba(255,255,255,0.8); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500;">
<i class="fas fa-arrow-left"></i> Back to Events
</a>
<h1 class="section-title serif" style="color: white; font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">{}</h1>
<div style="display: flex; flex-wrap: wrap; gap: 2rem; align-items: center; opacity: 0.9;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold);"></i>
<span>{}</span>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold);"></i>
{}
</div>
{}
</div>
</div>
</section>
{}
<!-- Event Details -->
<section class="section-2025">
<div class="container-2025">
<div style="display: grid; grid-template-columns: 1fr 300px; gap: 3rem; align-items: start;">
<!-- Main Content -->
<div>
<div class="card-2025 scroll-reveal">
<h3 class="card-title" style="margin-bottom: 2rem;">Event Description</h3>
<div style="line-height: 1.7; color: var(--medium-gray);">
{}
</div>
</div>
</div>
<!-- Sidebar -->
<div>
<div class="card-2025 scroll-reveal" style="margin-bottom: 2rem;">
<h4 class="card-title" style="font-size: 1.2rem; margin-bottom: 1.5rem;">Event Details</h4>
<div style="margin-bottom: 1.5rem;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold); width: 20px;"></i>
<strong style="color: var(--deep-navy);">Date & Time</strong>
</div>
<p style="color: var(--medium-gray); margin-left: 2rem; line-height: 1.5;">{}</p>
</div>
<div style="margin-bottom: 1.5rem;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold); width: 20px;"></i>
<strong style="color: var(--deep-navy);">Location</strong>
</div>
<p style="color: var(--medium-gray); margin-left: 2rem; line-height: 1.5;">
{}
</p>
{}
</div>
</div>
<div class="card-2025 scroll-reveal" style="text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 1.5rem;">
<i class="fas fa-envelope"></i>
</div>
<h4 class="card-title" style="font-size: 1.1rem; margin-bottom: 1rem;">Need More Info?</h4>
<p style="color: var(--medium-gray); margin-bottom: 1.5rem; font-size: 0.95rem;">Contact us for additional details about this event.</p>
<a href="/contact" class="btn-2025 btn-primary">
<i class="fas fa-phone"></i> Contact Us
</a>
</div>
</div>
</div>
</div>
</section>
<!-- Mobile Layout Adjustments -->
<style>
@media (max-width: 768px) {{
.container-2025 > div[style*="grid-template-columns"] {{
grid-template-columns: 1fr !important;
gap: 2rem !important;
}}
}}
</style>
"#,
event.title,
format_event_datetime(&event.start_time, &event.end_time),
// Location with optional URL
if let Some(ref url) = event.location_url {
if !url.is_empty() {
format!(r#"<a href="{}" target="_blank" style="color: white; text-decoration: none; font-weight: 500;">
{} <i class="fas fa-external-link-alt" style="font-size: 0.8rem; margin-left: 0.25rem;"></i>
</a>"#, url, event.location)
} else {
format!("<span>{}</span>", event.location)
}
} else {
format!("<span>{}</span>", event.location)
},
// Category badge
if let Some(ref category) = event.category {
if !category.is_empty() {
format!(r#"
<div style="background: var(--gradient-gold); color: var(--midnight); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.9rem; font-weight: 600;">
{}
</div>
"#, category)
} else {
String::new()
}
} else {
String::new()
},
// Event image section
if let Some(ref image) = event.image {
if !image.is_empty() {
format!(r#"
<section class="section-2025" style="padding: 2rem 0;">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="padding: 1rem; text-align: center;">
<img src="{}" alt="{}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;">
</div>
</div>
</section>
"#, image, event.title)
} else {
String::new()
}
} else {
String::new()
},
// Description
event.description.as_ref()
.map(|d| d.as_str())
.unwrap_or("<p>No description available for this event.</p>"),
// Sidebar date/time
format_event_datetime(&event.start_time, &event.end_time),
// Sidebar location
if let Some(ref url) = event.location_url {
if !url.is_empty() {
format!(r#"<a href="{}" target="_blank" style="color: var(--soft-gold); text-decoration: none; font-weight: 500;">
{} <i class="fas fa-external-link-alt" style="font-size: 0.8rem; margin-left: 0.25rem;"></i>
</a>"#, url, event.location)
} else {
event.location.clone()
}
} else {
event.location.clone()
},
// Get directions button
if let Some(ref url) = event.location_url {
if !url.is_empty() {
format!(r#"
<a href="{}" target="_blank" class="btn-2025 btn-outline" style="margin-left: 2rem; margin-top: 0.5rem; font-size: 0.9rem; padding: 0.5rem 1rem;">
<i class="fas fa-directions"></i> Get Directions
</a>
"#, url)
} else {
String::new()
}
} else {
String::new()
}
);
Html(layout(&content, &event.title))
},
Ok(None) => {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Event Not Found</h1>
<p>The requested event could not be found.</p>
<a href="/events" class="btn-2025 btn-primary"> Back to Events</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Event Not Found"))
},
Err(_) => {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Error</h1>
<p>Unable to load event. Please try again later.</p>
<a href="/events" class="btn-2025 btn-primary"> Back to Events</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Error"))
}
}
}
fn render_event_card(event: &crate::models::Event, is_featured: bool, index: usize) -> String {
let description = event.description.as_ref()
.map(|d| {
let stripped = strip_html(d);
if stripped.len() > 120 {
format!("{}...", &stripped[..120])
} else {
stripped
}
})
.unwrap_or_else(|| String::new());
let formatted_time = format_event_datetime(&event.start_time, &event.end_time);
format!(r#"
<a href="/events/{}" style="text-decoration: none; color: inherit; display: block;">
<div class="event-card-2025 scroll-reveal stagger-{}" id="{}" {}>
{}
<div class="event-image-2025" {}></div>
<div class="event-content-2025">
<h3 class="event-title" style="margin-bottom: 1.5rem;">{}</h3>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-clock" style="color: var(--soft-gold);"></i>
{}
</p>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold);"></i>
{}
</p>
<div style="margin: 1.5rem 0; color: var(--medium-gray); line-height: 1.6;">
{}
</div>
<div class="btn-2025 btn-primary" style="background: var(--gradient-gold) !important; color: var(--midnight) !important; display: inline-flex; align-items: center; gap: 0.5rem;">
View Details <i class="fas fa-arrow-right"></i>
</div>
</div>
</div>
</a>
"#,
event.id,
(index % 3) + 1,
event.id,
if is_featured {
r#"style="border: 2px solid var(--soft-gold); cursor: pointer;""#
} else {
r#"style="cursor: pointer;""#
},
if is_featured {
r#"<div style="background: var(--gradient-gold); color: var(--midnight); padding: 0.75rem; text-align: center; font-weight: 600; font-size: 0.9rem;">⭐ FEATURED EVENT</div>"#
} else {
""
},
event.image.as_ref()
.filter(|img| !img.is_empty())
.map(|img| format!(r#"style="background-image: url('{}'); background-size: cover; background-position: center;""#, img))
.unwrap_or_default(),
event.title,
formatted_time,
if let Some(ref url) = event.location_url {
if !url.is_empty() {
format!(r#"
<span onclick="event.stopPropagation(); window.open('{}', '_blank');" style="color: var(--soft-gold); cursor: pointer; text-decoration: underline;">
{} <i class="fas fa-external-link-alt" style="font-size: 0.8rem; margin-left: 0.25rem;"></i>
</span>
"#, url, event.location)
} else {
event.location.clone()
}
} else {
event.location.clone()
},
description
)
}

View file

@ -1,460 +0,0 @@
use axum::response::Html;
use crate::services::{ApiService, format_event_datetime, strip_html};
use crate::layout::layout;
pub async fn home_handler() -> Html<String> {
let api_service = ApiService::new();
// Fetch data concurrently for better performance
let (config, bulletin, events, verse) = tokio::join!(
api_service.get_config(),
api_service.get_current_bulletin(),
api_service.get_events(Some(3)),
api_service.get_random_verse()
);
let config = config.unwrap_or(None);
let current_bulletin = bulletin.unwrap_or(None);
let upcoming_events = events.unwrap_or_default();
let bible_verse = verse.unwrap_or(None);
let church_info = config.as_ref();
let church_name = church_info
.and_then(|c| c.church_name.as_ref())
.map(|s| s.as_str())
.unwrap_or("Rockville Tolland SDA Church");
let about_text = church_info
.and_then(|c| c.about_text.as_ref())
.map(|s| s.as_str())
.unwrap_or("Proclaiming the Three Angels' Messages with Love and Hope. Join our community of faith as we worship together and grow in Christ.");
let content = format!(r#"
<!-- 2025 Hero Section -->
<section class="hero-2025">
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title serif">
Welcome to<br>
{}
</h1>
<p class="hero-subtitle">
{}
</p>
<div class="hero-cta-group">
<a href="/events" class="btn-2025 btn-primary">
<i class="fas fa-calendar-alt"></i>
Join Us This Sabbath
</a>
<a href="/sermons" class="btn-2025 btn-outline">
<i class="fas fa-play"></i>
Watch Online
</a>
</div>
<div style="display: flex; gap: 2rem; margin-top: 2rem; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 0.75rem; color: rgba(255,255,255,0.9);">
<div style="width: 12px; height: 12px; background: var(--soft-gold); border-radius: 50%;"></div>
<span style="font-size: 0.95rem;">Sabbath School 9:30 AM</span>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem; color: rgba(255,255,255,0.9);">
<div style="width: 12px; height: 12px; background: var(--soft-gold); border-radius: 50%;"></div>
<span style="font-size: 0.95rem;">Divine Worship 11:00 AM</span>
</div>
</div>
</div>
<div class="hero-visual">
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-dove" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">First Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Fear God & Give Glory</p>
</div>
</div>
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-bullhorn" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">Second Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Babylon is Fallen</p>
</div>
</div>
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-scroll" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">Third Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Keep God's Commands</p>
</div>
</div>
</div>
</div>
</section>
<!-- Three Angels Messages 2025 -->
<section class="section-2025 angels-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title serif">The Three Angels' Messages</h2>
<p class="section-subtitle">Central to our mission as Seventh-day Adventists, these messages from Revelation 14 guide our purpose and calling.</p>
</div>
<div class="angels-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-dove"></i>
</div>
<h3 class="card-title">First Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Fear God and give glory to Him, for the hour of His judgment has come; and worship Him who made heaven and earth, the sea and springs of water."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:6-7)</cite>
<p class="card-text" style="margin-top: 1.5rem;">The everlasting gospel calls all people to worship the Creator God who made heaven and earth, recognizing His authority and giving Him glory.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-bullhorn"></i>
</div>
<h3 class="card-title">Second Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Babylon is fallen, is fallen, that great city, because she has made all nations drink of the wine of the wrath of her fornication."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:8)</cite>
<p class="card-text" style="margin-top: 1.5rem;">A warning about false religious systems and a call to come out of spiritual confusion, choosing truth over tradition.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h3 class="card-title">Third Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Here is the patience of the saints; here are those who keep the commandments of God and the faith of Jesus."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:12)</cite>
<p class="card-text" style="margin-top: 1.5rem;">A call to remain faithful to God's commandments, including the seventh-day Sabbath, while maintaining faith in Jesus Christ.</p>
</div>
</div>
</div>
</section>
{}
<!-- Service Times 2025 -->
<section class="section-2025 services-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title" style="color: white;">Service Times</h2>
<p class="section-subtitle" style="color: rgba(255,255,255,0.8);">Join us for worship and fellowship</p>
</div>
<div class="services-grid">
<div class="service-card-2025 scroll-reveal stagger-1">
<div class="service-icon-2025">
<i class="fas fa-book"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Sabbath School</h3>
<div class="service-time">9:30 AM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Join us for Bible study and fellowship every Sabbath morning</p>
</div>
<div class="service-card-2025 scroll-reveal stagger-2">
<div class="service-icon-2025">
<i class="fas fa-praying-hands"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Divine Worship</h3>
<div class="service-time">11:00 AM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Worship service with inspiring sermons and uplifting music</p>
</div>
<div class="service-card-2025 scroll-reveal stagger-3">
<div class="service-icon-2025">
<i class="fas fa-hands-praying"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Prayer Meeting</h3>
<div class="service-time">Wed 7:00 PM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Mid-week spiritual refreshment with prayer and Bible study</p>
</div>
</div>
</div>
</section>
{}
{}
<!-- Our Beliefs 2025 -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Our Core Beliefs</h2>
<p class="section-subtitle">As Seventh-day Adventists, we accept the Bible as our only creed and hold certain fundamental beliefs to be the teaching of the Holy Scriptures.</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-bible"></i>
</div>
<h4 class="card-title">The Holy Scriptures</h4>
<p class="card-text">The Holy Scriptures are the infallible revelation of God's will and the authoritative revealer of doctrines.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-cross"></i>
</div>
<h4 class="card-title">The Trinity</h4>
<p class="card-text">There is one God: Father, Son, and Holy Spirit, a unity of three co-eternal Persons.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h4 class="card-title">The Sabbath</h4>
<p class="card-text">The seventh day of the week is the Sabbath of the Lord our God, a day of rest and worship.</p>
</div>
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-cloud"></i>
</div>
<h4 class="card-title">The Second Coming</h4>
<p class="card-text">The second coming of Christ is the blessed hope of the church and the grand climax of the gospel.</p>
</div>
</div>
</div>
</section>
<!-- Mobile App Section -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Faith in Your Pocket</h2>
<p class="section-subtitle">Access sermons, events, and stay connected with our church family through our mobile app designed for spiritual growth.</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-mobile-alt"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 2rem;">Download Our Mobile App</h3>
<p style="color: var(--medium-gray); line-height: 1.6; margin-bottom: 3rem; font-size: 1.1rem;">
Stay connected with sermons, events, and church activities wherever you go.
Our app makes it easy to access spiritual content and stay engaged with our community.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center; align-items: center;">
<!-- iOS App Store -->
<a href="https://apps.apple.com/us/app/rtsda/id6738595657" target="_blank" rel="noopener noreferrer" style="display: block; transition: transform 0.3s ease;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
<img src="/images/app-store-badge.svg" alt="Download on the App Store" style="height: 60px; border-radius: 8px;">
</a>
<!-- Android APK Download -->
<button onclick="downloadApk()" class="btn-2025 btn-primary" style="display: flex; align-items: center; gap: 1rem; font-size: 1rem; padding: 1rem 1.5rem; background: var(--deep-navy); border: none; cursor: pointer;">
<svg viewBox="0 0 24 24" style="height: 40px; width: 40px; fill: var(--soft-gold);">
<path d="M17.6 9.48l1.84-3.18c.16-.31.04-.69-.26-.85a.637.637 0 0 0-.83.22l-1.88 3.24a11.463 11.463 0 0 0-8.94 0L5.65 5.67a.643.643 0 0 0-.87-.2c-.28.18-.37.54-.22.83L6.4 9.48A10.78 10.78 0 0 0 1 18h22a10.78 10.78 0 0 0-5.4-8.52zM7 15.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5zm10 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5z"/>
</svg>
<div style="text-align: left; color: white;">
<div style="font-size: 0.8rem; opacity: 0.8;">DOWNLOAD APK</div>
<div style="font-size: 1.2rem; font-weight: 600;">Android</div>
</div>
</button>
</div>
<div style="margin-top: 2rem; padding: 1.5rem; background: rgba(255,255,255,0.8); border-radius: 12px; border-left: 4px solid var(--soft-gold);">
<p style="color: var(--deep-navy); margin: 0; font-style: italic;">
<i class="fas fa-info-circle" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
Available on both iOS and Android platforms. Download today to access sermons, events, and stay connected with our church community.
</p>
</div>
</div>
</div>
</section>
<script>
function downloadApk() {{
window.location.href = 'https://api.rockvilletollandsda.church/uploads/rtsda_android/current';
}}
</script>
"#,
church_name,
about_text,
// Bible verse section
if let Some(verse) = &bible_verse {
format!(r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 class="section-title" style="font-size: 2rem; margin-bottom: 2rem;">Today's Scripture</h3>
<blockquote style="font-family: 'Playfair Display', serif; font-size: 1.4rem; font-style: italic; color: var(--deep-navy); margin: 2rem 0; line-height: 1.6;">
"{}"
</blockquote>
<cite style="color: var(--medium-gray); font-size: 1.1rem; font-weight: 600;">- {}</cite>
</div>
</div>
</section>
"#, verse.text, verse.reference)
} else {
String::new()
},
// Current bulletin section
if let Some(bulletin) = &current_bulletin {
let formatted_date = format!("{}", bulletin.date); // You can add date formatting here
format!(r#"
<!-- This Week's Bulletin -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">This Week's Bulletin</h2>
<p class="section-subtitle">Stay informed about church activities and worship</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-file-alt"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 1rem;">{}</h3>
<p style="color: var(--medium-gray); font-size: 1.1rem; margin-bottom: 2rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
{}
<div style="display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; margin-top: 2rem;">
{}
<a href="/bulletins/{}" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-eye"></i>
View Details
</a>
<a href="/bulletins" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-archive"></i>
View Archive
</a>
</div>
</div>
</div>
</section>
"#,
bulletin.title,
formatted_date,
if let Some(ref scripture) = bulletin.scripture_reading {
if !scripture.is_empty() {
format!(r#"
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy);">
<strong style="color: var(--deep-navy);">Scripture Reading:</strong><br>
{}
</div>
"#, scripture)
} else {
String::new()
}
} else {
String::new()
},
if let Some(ref pdf_path) = bulletin.pdf_path {
if !pdf_path.is_empty() {
format!(r#"
<a href="{}" target="_blank" class="btn-2025 btn-primary">
<i class="fas fa-download"></i>
Download PDF
</a>
"#, pdf_path)
} else {
String::new()
}
} else {
String::new()
},
bulletin.id
)
} else {
String::new()
},
// Upcoming events section
if !upcoming_events.is_empty() {
let events_html = upcoming_events.iter().enumerate().map(|(index, event)| {
let description = event.description.as_ref()
.map(|d| {
let stripped = strip_html(d);
if stripped.len() > 120 {
format!("{}...", &stripped[..120])
} else {
stripped
}
})
.unwrap_or_else(|| "Join us for this special event.".to_string());
let formatted_time = format_event_datetime(&event.start_time, &event.end_time);
format!(r#"
<a href="/events/{}" style="text-decoration: none; color: inherit; display: block;">
<div class="event-card-2025 scroll-reveal stagger-{}" style="cursor: pointer;">
<div class="event-image-2025" {}></div>
<div class="event-content-2025">
<h3 class="event-title" style="margin-bottom: 1.5rem;">{}</h3>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-clock" style="color: var(--soft-gold);"></i>
{}
</p>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold);"></i>
{}
</p>
<div style="margin: 1.5rem 0; color: var(--medium-gray); line-height: 1.6;">
{}
</div>
<div class="btn-2025 btn-primary" style="background: var(--gradient-gold) !important; color: var(--midnight) !important; display: inline-flex; align-items: center; gap: 0.5rem;">
View Details <i class="fas fa-arrow-right"></i>
</div>
</div>
</div>
</a>
"#,
event.id,
(index % 3) + 1,
event.image.as_ref()
.filter(|img| !img.is_empty())
.map(|img| format!(r#"style="background-image: url('{}'); background-size: cover; background-position: center;""#, img))
.unwrap_or_default(),
event.title,
formatted_time,
event.location,
description
)
}).collect::<Vec<_>>().join("");
format!(r#"
<!-- Upcoming Events -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Upcoming Events</h2>
<p class="section-subtitle">Join us for these special occasions and activities</p>
</div>
<div class="events-grid">
{}
</div>
<div style="text-align: center; margin-top: 3rem;">
<a href="/events" class="btn-2025 btn-primary" style="font-size: 1.1rem; padding: 1rem 3rem;">
<i class="fas fa-calendar" style="margin-right: 0.5rem;"></i>
View All Events
</a>
</div>
</div>
</section>
"#, events_html)
} else {
String::new()
}
);
Html(layout(&content, "Home"))
}

View file

@ -1,185 +0,0 @@
use axum::response::Html;
use crate::layout::layout;
pub async fn ministries_handler() -> Html<String> {
let content = r#"
<!-- Ministries Page -->
<section class="section-2025" style="padding-top: 6rem;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="font-size: 3rem; margin-bottom: 2rem;">Our Ministries</h1>
<p class="section-subtitle" style="max-width: 800px; margin: 0 auto 4rem auto;">
Discover the various ways you can get involved, grow spiritually, and serve in our church community.
Each ministry is designed to help believers grow in faith and share God's love with others.
</p>
</div>
<!-- Ministries Grid -->
<div class="ministries-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 2rem; margin-top: 3rem;">
<!-- Prayer Ministry -->
<div class="card-2025 scroll-reveal" style="display: flex; flex-direction: column; height: 100%;">
<div style="aspect-ratio: 4/3; width: 100%; overflow: hidden; border-radius: 16px 16px 0 0; position: relative;">
<img src="/images/events/prayer.webp" alt="Prayer Ministry" style="width: 100%; height: 100%; object-fit: cover;">
<div style="position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);"></div>
</div>
<div style="padding: 2rem; flex-grow: 1; display: flex; flex-direction: column;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Prayer Ministry</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 1.5rem; flex-grow: 1;">
Join one of our many prayer groups or submit your prayer requests. We have multiple opportunities for prayer throughout the week: Daily Prayer Group, Wednesday Prayer Group, BiWeekly Prayer Group, and Monthly Prayer Group.
</p>
<div style="margin-bottom: 1.5rem; padding: 1.5rem; background: var(--soft-gray); border-radius: 12px;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; color: var(--deep-navy);">
<i class="fas fa-calendar" style="color: var(--soft-gold); font-size: 1.1rem;"></i>
<span>Daily, Weekly, BiWeekly, and Monthly Groups</span>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; color: var(--deep-navy);">
<i class="fas fa-clock" style="color: var(--soft-gold); font-size: 1.1rem;"></i>
<span>Various Times Available</span>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem; color: var(--deep-navy);">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold); font-size: 1.1rem;"></i>
<span>In Person & Online</span>
</div>
</div>
<a href="/contact" class="btn-2025 btn-primary" style="margin-top: auto;">
<i class="fas fa-envelope" style="margin-right: 0.5rem;"></i>
Contact Prayer Ministry
</a>
</div>
</div>
<!-- Gardening Ministry -->
<div class="card-2025 scroll-reveal" style="display: flex; flex-direction: column; height: 100%;">
<div style="aspect-ratio: 4/3; width: 100%; overflow: hidden; border-radius: 16px 16px 0 0; position: relative;">
<img src="/images/garden.webp" alt="Gardening Ministry" style="width: 100%; height: 100%; object-fit: cover;">
<div style="position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);"></div>
</div>
<div style="padding: 2rem; flex-grow: 1; display: flex; flex-direction: column;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Gardening Ministry</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 1.5rem; flex-grow: 1;">
Learn about sustainable gardening practices and join our community of gardeners. Watch our gardening series to learn practical tips and techniques for growing your own food.
</p>
<a href="https://www.youtube.com/playlist?list=PLtVXQJggBMd8p00o3vo5MsGizRy1Dkwca" target="_blank" rel="noopener noreferrer" class="btn-2025 btn-primary" style="margin-top: auto;">
<i class="fas fa-video" style="margin-right: 0.5rem;"></i>
Garden Video Series
</a>
</div>
</div>
<!-- Bible Studies -->
<div class="card-2025 scroll-reveal" style="display: flex; flex-direction: column; height: 100%;">
<div style="aspect-ratio: 4/3; width: 100%; overflow: hidden; border-radius: 16px 16px 0 0; position: relative;">
<img src="/images/bible.webp" alt="Bible Studies" style="width: 100%; height: 100%; object-fit: cover;">
<div style="position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);"></div>
</div>
<div style="padding: 2rem; flex-grow: 1; display: flex; flex-direction: column;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Bible Studies</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 1.5rem; flex-grow: 1;">
Deepen your understanding of Scripture through our Bible study programs and resources. Access free Bible study guides and tools to enhance your spiritual journey.
</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem; margin-top: auto;">
<a href="/contact" class="btn-2025 btn-primary" style="font-size: 0.9rem; padding: 0.75rem 1rem; text-align: center;">
<i class="fas fa-envelope" style="margin-right: 0.5rem;"></i>
Request Studies
</a>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
<a href="https://www.amazingfacts.org/media-library/read/c/2/t/bible-study-guides" target="_blank" rel="noopener noreferrer" class="btn-2025 btn-outline" style="font-size: 0.8rem; padding: 0.5rem 0.75rem; flex: 1; min-width: 100px; text-align: center; background: white; color: var(--deep-navy); border: 1px solid var(--deep-navy);">
<i class="fas fa-external-link-alt" style="margin-right: 0.25rem;"></i>
Guides
</a>
<a href="https://www.e-sword.net/" target="_blank" rel="noopener noreferrer" class="btn-2025 btn-outline" style="font-size: 0.8rem; padding: 0.5rem 0.75rem; flex: 1; min-width: 100px; text-align: center; background: white; color: var(--deep-navy); border: 1px solid var(--deep-navy);">
<i class="fas fa-external-link-alt" style="margin-right: 0.25rem;"></i>
E-Sword
</a>
<a href="https://bibleproject.com/app/" target="_blank" rel="noopener noreferrer" class="btn-2025 btn-outline" style="font-size: 0.8rem; padding: 0.5rem 0.75rem; flex: 1; min-width: 100px; text-align: center; background: white; color: var(--deep-navy); border: 1px solid var(--deep-navy);">
<i class="fas fa-external-link-alt" style="margin-right: 0.25rem;"></i>
Bible Project
</a>
</div>
</div>
</div>
</div>
<!-- Adventist Youth -->
<div class="card-2025 scroll-reveal" style="display: flex; flex-direction: column; height: 100%;">
<div style="aspect-ratio: 4/3; width: 100%; overflow: hidden; border-radius: 16px 16px 0 0; position: relative;">
<img src="/images/ay.webp" alt="Adventist Youth" style="width: 100%; height: 100%; object-fit: cover;">
<div style="position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);"></div>
</div>
<div style="padding: 2rem; flex-grow: 1; display: flex; flex-direction: column;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Adventist Youth</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 1.5rem; flex-grow: 1;">
Join our vibrant youth community for spiritual growth, fellowship, and service opportunities. Experience the joy of growing in faith with other young believers.
</p>
<a href="/contact" class="btn-2025 btn-primary" style="margin-top: auto;">
<i class="fas fa-envelope" style="margin-right: 0.5rem;"></i>
Contact Youth Ministry
</a>
</div>
</div>
<!-- Health Ministry -->
<div class="card-2025 scroll-reveal" style="display: flex; flex-direction: column; height: 100%;">
<div style="aspect-ratio: 4/3; width: 100%; overflow: hidden; border-radius: 16px 16px 0 0; position: relative;">
<img src="/images/health.webp" alt="Health Ministry" style="width: 100%; height: 100%; object-fit: cover;">
<div style="position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);"></div>
</div>
<div style="padding: 2rem; flex-grow: 1; display: flex; flex-direction: column;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Health Ministry</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 1.5rem; flex-grow: 1;">
Discover resources and programs promoting physical, mental, and spiritual well-being through our health ministry. Learn about God's plan for optimal health.
</p>
<a href="https://www.healthministries.com/articles/" target="_blank" rel="noopener noreferrer" class="btn-2025 btn-primary" style="margin-top: auto;">
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
Health Resources
</a>
</div>
</div>
<!-- Training Ministry -->
<div class="card-2025 scroll-reveal" style="display: flex; flex-direction: column; height: 100%;">
<div style="aspect-ratio: 4/3; width: 100%; overflow: hidden; border-radius: 16px 16px 0 0; position: relative;">
<img src="/images/training.webp" alt="Training Ministry" style="width: 100%; height: 100%; object-fit: cover;">
<div style="position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent);"></div>
</div>
<div style="padding: 2rem; flex-grow: 1; display: flex; flex-direction: column;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Training & Education</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 1.5rem; flex-grow: 1;">
Develop your spiritual gifts and ministry skills through our training programs. Learn to share your faith effectively and serve others with confidence.
</p>
<a href="/contact" class="btn-2025 btn-primary" style="margin-top: auto;">
<i class="fas fa-envelope" style="margin-right: 0.5rem;"></i>
Learn More
</a>
</div>
</div>
</div>
<!-- Call to Action -->
<div style="text-align: center; margin-top: 4rem;">
<div class="card-2025 scroll-reveal" style="max-width: 600px; margin: 0 auto; padding: 3rem;">
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2rem; margin-bottom: 1rem;">Get Involved Today</h3>
<p style="color: var(--deep-navy); line-height: 1.6; margin-bottom: 2rem;">
Ready to join one of our ministries? Contact us to learn more about how you can get involved and make a difference in our community.
</p>
<a href="/contact" class="btn-2025 btn-primary" style="font-size: 1.1rem; padding: 1rem 2rem;">
<i class="fas fa-phone" style="margin-right: 0.5rem;"></i>
Contact Us Today
</a>
</div>
</div>
</div>
</section>
"#;
Html(layout(content, "Our Ministries"))
}

View file

@ -1,7 +0,0 @@
pub mod home;
pub mod about;
pub mod ministries;
pub mod sermons;
pub mod events;
pub mod bulletins;
pub mod contact;

View file

@ -1,757 +0,0 @@
use axum::{extract::{Path, Query}, response::Html};
use crate::layout::layout;
use crate::services::{ApiService, parse_sermon_title, format_duration, format_date};
use serde::Deserialize;
use std::collections::HashMap;
use chrono::Datelike;
#[derive(Deserialize)]
pub struct ArchiveQuery {
collection: Option<String>,
}
pub async fn sermons_handler() -> Html<String> {
let api_service = ApiService::new();
match api_service.get_jellyfin_libraries().await {
Ok(libraries) => {
if libraries.is_empty() {
return render_no_sermons_page();
}
let mut collection_data = std::collections::HashMap::new();
for library in &libraries {
match api_service.get_jellyfin_sermons(Some(&library.id), Some(6)).await {
Ok(mut sermons) => {
// Sort sermons by date
sermons.sort_by(|a, b| {
let get_valid_date = |sermon: &crate::models::JellyfinItem| {
let parsed = parse_sermon_title(&sermon.name);
if let Some(ref date_str) = parsed.date_from_title {
if let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
return date;
}
}
if let Some(ref premiere_date) = sermon.premiere_date {
if let Ok(date) = chrono::NaiveDate::parse_from_str(&premiere_date.split('T').next().unwrap_or(""), "%Y-%m-%d") {
return date;
}
}
chrono::Utc::now().naive_utc().date()
};
let date_a = get_valid_date(a);
let date_b = get_valid_date(b);
date_b.cmp(&date_a)
});
collection_data.insert(library.name.clone(), sermons);
},
Err(_) => {
collection_data.insert(library.name.clone(), vec![]);
}
}
}
Html(layout(&render_sermons_content(collection_data), "Latest Sermons & Live Streams"))
},
Err(_) => render_no_sermons_page()
}
}
fn render_no_sermons_page() -> Html<String> {
let content = r#"
<!-- Sermons Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Latest Sermons & Live Streams</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Listen to our most recent inspiring messages from God's Word</p>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-microphone-alt"></i>
</div>
<h3 class="card-title">Sermons Coming Soon</h3>
<p class="card-text">Sermons are currently being prepared for online streaming.</p>
<p class="card-text">Please check back later or contact us for more information.</p>
<p style="color: var(--medium-gray); font-style: italic; margin-top: 1rem;"><em>Note: Make sure Jellyfin server credentials are configured properly.</em></p>
</div>
</div>
</section>
"#;
Html(layout(content, "Sermons"))
}
fn render_sermons_content(collection_data: std::collections::HashMap<String, Vec<crate::models::JellyfinItem>>) -> String {
format!(r#"
<!-- Sermons Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Latest Sermons & Live Streams</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Listen to our most recent inspiring messages from God's Word. These are the latest sermons and live stream recordings for your spiritual growth.</p>
<div style="text-align: center; margin-top: 2rem;">
<a href="/sermons/archive" class="btn-2025 btn-outline" style="color: white; border-color: rgba(255,255,255,0.5); background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); font-size: 1.1rem; padding: 1rem 2rem;">
<i class="fas fa-archive" style="margin-right: 0.5rem;"></i>
Browse Complete Archive
</a>
</div>
</div>
</div>
</section>
{}
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">About Our Sermons</h2>
<p class="section-subtitle">Our sermons focus on the Three Angels' Messages and the teachings of Jesus Christ. Each message is designed to strengthen your faith and deepen your understanding of God's Word.</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h3 class="card-title">Sabbath Sermons</h3>
<p class="card-text">Weekly messages during Divine Worship</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h3 class="card-title">Prophecy Studies</h3>
<p class="card-text">Deep dives into Biblical prophecy</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-heart"></i>
</div>
<h3 class="card-title">Practical Christianity</h3>
<p class="card-text">Applying Bible principles to daily life</p>
</div>
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-star"></i>
</div>
<h3 class="card-title">Special Events</h3>
<p class="card-text">Revival meetings and guest speakers</p>
</div>
</div>
</div>
</section>
"#,
collection_data.iter().enumerate().map(|(collection_index, (collection_name, sermons))| {
format!(r#"
<section class="section-2025" style="{}">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title" style="display: flex; align-items: center; gap: 1rem;">
<i class="fas fa-{}" style="color: var(--soft-gold);"></i>
{}
</h2>
<p class="section-subtitle">
{}
</p>
<div style="text-align: center; margin-top: 2rem;">
<a href="/sermons/archive?collection={}" class="btn-2025 btn-outline" style="color: var(--deep-navy); border-color: var(--deep-navy);">
<i class="fas fa-archive" style="margin-right: 0.5rem;"></i>
View All {}
</a>
</div>
</div>
{}
</div>
</section>
"#,
if collection_index > 0 { "background: var(--soft-gray);" } else { "" },
if collection_name == "LiveStreams" { "broadcast-tower" } else { "church" },
if collection_name == "LiveStreams" { "Live Stream Recordings" } else { "Sabbath Sermons" },
if collection_name == "LiveStreams" {
"Recorded live streams from our worship services and special events"
} else {
"Messages from our regular Sabbath worship services"
},
collection_name,
if collection_name == "LiveStreams" { "Live Streams" } else { "Sermons" },
if sermons.is_empty() {
format!(r#"
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 2rem;">
<h4 style="color: var(--deep-navy); margin-bottom: 1rem;">No {} Available</h4>
<p style="color: var(--medium-gray);">Check back later for new content in this collection.</p>
</div>
"#, collection_name)
} else {
format!(r#"
<div class="cards-grid">
{}
</div>
"#, sermons.iter().enumerate().map(|(index, sermon)| {
let parsed = parse_sermon_title(&sermon.name);
format!(r#"
<div class="card-2025 scroll-reveal stagger-{}">
<div class="card-icon-2025">
<i class="fas fa-{}"></i>
</div>
<h3 class="card-title">{}</h3>
{}
{}
{}
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap;">
{}
<span style="background: var(--soft-gold); color: var(--deep-navy); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.9rem; font-weight: 500;">
<i class="fas fa-{}" style="margin-right: 0.25rem;"></i>
{}
</span>
</div>
<a href="/sermons/{}" class="btn-2025 btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-{}"></i>
{} {}
</a>
</div>
"#,
(index % 3) + 1,
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "music" } else { "video" },
parsed.title,
if let Some(ref speaker) = parsed.speaker {
format!(r#"
<p style="color: var(--soft-gold); margin-bottom: 1rem; font-weight: 500;">
<i class="fas fa-user" style="margin-right: 0.5rem;"></i>{}
</p>
"#, speaker)
} else {
String::new()
},
if let Some(ref premiere_date) = sermon.premiere_date {
format!(r#"
<p style="color: var(--medium-gray); margin-bottom: 1rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
"#, format_date(premiere_date))
} else if let Some(ref date_from_title) = parsed.date_from_title {
format!(r#"
<p style="color: var(--medium-gray); margin-bottom: 1rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
"#, date_from_title)
} else {
String::new()
},
if let Some(ref overview) = sermon.overview {
let preview = if overview.len() > 150 {
format!("{}...", &overview[..150])
} else {
overview.clone()
};
format!(r#"
<p style="color: var(--medium-gray); margin-bottom: 1.5rem; line-height: 1.5;">
{}
</p>
"#, preview)
} else {
String::new()
},
if let Some(ticks) = sermon.run_time_ticks {
format!(r#"
<span style="background: var(--soft-gray); color: var(--deep-navy); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.9rem; font-weight: 500;">
<i class="fas fa-clock" style="margin-right: 0.25rem;"></i>
{}
</span>
"#, format_duration(ticks))
} else {
String::new()
},
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "music" } else { "video" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "Audio" } else { "Video" },
sermon.id,
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "play" } else { "play-circle" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "Listen" } else { "Watch" },
if collection_name == "LiveStreams" { "Recording" } else { "Sermon" }
)
}).collect::<Vec<_>>().join(""))
})
}).collect::<Vec<_>>().join(""))
}
pub async fn sermon_detail_handler(Path(id): Path<String>) -> Html<String> {
let api_service = ApiService::new();
match api_service.get_jellyfin_sermon(&id).await {
Ok(Some(sermon)) => {
match api_service.authenticate_jellyfin().await {
Ok(Some((token, _))) => {
let parsed = parse_sermon_title(&sermon.name);
let stream_url = api_service.get_jellyfin_stream_url(&sermon.id, &token);
let content = format!(r#"
<!-- Sermon Detail Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 4rem 0 3rem 0;">
<div class="container-2025">
<a href="/sermons" style="color: rgba(255,255,255,0.8); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500; margin-top: 1rem;">
<i class="fas fa-arrow-left"></i> Back to Sermons
</a>
<h1 class="section-title serif" style="color: white; font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">{}</h1>
{}
<div style="display: flex; gap: 2rem; flex-wrap: wrap; color: rgba(255,255,255,0.9);">
{}
{}
</div>
</div>
</section>
<!-- Sermon Player -->
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 900px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-{}"></i>
</div>
<h3 class="card-title">{}</h3>
{}
</div>
</div>
</section>
{}
<!-- Share Section -->
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 600px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-share-alt"></i>
</div>
<h3 class="card-title">Share This Sermon</h3>
<p class="card-text">Invite others to listen to this inspiring message:</p>
<button onclick="copyToClipboard(window.location.href)" class="btn-2025 btn-primary" style="margin-top: 1rem;">
<i class="fas fa-copy"></i>
Copy Link
</button>
</div>
</div>
</section>
<script>
function copyToClipboard(text) {{
navigator.clipboard.writeText(text).then(function() {{
alert('Link copied to clipboard!');
}});
}}
</script>
"#,
parsed.title,
if let Some(ref speaker) = parsed.speaker {
format!(r#"
<p style="color: var(--soft-gold); font-size: 1.3rem; margin-bottom: 1rem; font-weight: 500;">
<i class="fas fa-user" style="margin-right: 0.5rem;"></i>Speaker: {}
</p>
"#, speaker)
} else {
String::new()
},
if let Some(ref premiere_date) = sermon.premiere_date {
format!(r#"
<p style="display: flex; align-items: center; gap: 0.5rem; font-size: 1.1rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold);"></i>
{}
</p>
"#, format_date(premiere_date))
} else if let Some(ref date_from_title) = parsed.date_from_title {
format!(r#"
<p style="display: flex; align-items: center; gap: 0.5rem; font-size: 1.1rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold);"></i>
{}
</p>
"#, date_from_title)
} else {
String::new()
},
if let Some(ticks) = sermon.run_time_ticks {
format!(r#"
<p style="display: flex; align-items: center; gap: 0.5rem; font-size: 1.1rem;">
<i class="fas fa-clock" style="color: var(--soft-gold);"></i>
{}
</p>
"#, format_duration(ticks))
} else {
String::new()
},
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "music" } else { "video" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "Audio Sermon" } else { "Video Sermon" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" {
format!(r#"
<audio src="{}" controls preload="auto" style="width: 100%; max-width: 600px; margin: 2rem 0; border-radius: 8px;" crossorigin="anonymous">
Your browser does not support the audio element.
</audio>
"#, stream_url)
} else {
format!(r#"
<video src="{}" controls playsinline webkit-playsinline preload="auto" x-webkit-airplay="allow" crossorigin="anonymous" style="width: 100%; max-width: 800px; margin: 2rem 0; border-radius: 8px;">
Your browser does not support the video element.
</video>
"#, stream_url)
},
if let Some(ref overview) = sermon.overview {
format!(r#"
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 800px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 class="card-title">Description</h3>
<p style="color: var(--medium-gray); line-height: 1.6; font-size: 1.1rem;">{}</p>
</div>
</div>
</section>
"#, overview)
} else {
String::new()
}
);
Html(layout(&content, &parsed.title))
},
_ => render_sermon_error("Unable to access sermon content. Please try again later.")
}
},
Ok(None) => render_sermon_error("The requested sermon could not be found."),
Err(_) => render_sermon_error("Unable to load sermon. Please try again later.")
}
}
fn render_sermon_error(message: &str) -> Html<String> {
let content = format!(r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Error</h1>
<p>{}</p>
<a href="/sermons" class="btn-2025 btn-primary"> Back to Sermons</a>
</div>
</div>
</section>
"#, message);
Html(layout(&content, "Error"))
}
pub async fn sermons_archive_handler(Query(params): Query<ArchiveQuery>) -> Html<String> {
let api_service = ApiService::new();
match api_service.get_jellyfin_libraries().await {
Ok(libraries) => {
// If a specific collection is selected, show only that one
if let Some(selected_collection) = params.collection {
if let Some(library) = libraries.iter().find(|lib| lib.name == selected_collection) {
match api_service.get_jellyfin_sermons(Some(&library.id), None).await {
Ok(sermons) => {
let organized = organize_sermons_by_year_month(&sermons);
let mut years: Vec<String> = organized.keys().cloned().collect();
years.sort_by(|a, b| b.parse::<i32>().unwrap_or(0).cmp(&a.parse::<i32>().unwrap_or(0)));
return render_archive_page(&organized, &years, &selected_collection, &libraries);
}
Err(_) => return render_error_page()
}
} else {
return render_error_page();
}
}
// Default to showing Sermons collection (skip multi-collection view)
let default_collection = libraries.iter()
.find(|lib| lib.name == "Sermons")
.or_else(|| libraries.first());
if let Some(library) = default_collection {
match api_service.get_jellyfin_sermons(Some(&library.id), None).await {
Ok(sermons) => {
let organized = organize_sermons_by_year_month(&sermons);
let mut years: Vec<String> = organized.keys().cloned().collect();
years.sort_by(|a, b| b.parse::<i32>().unwrap_or(0).cmp(&a.parse::<i32>().unwrap_or(0)));
return render_archive_page(&organized, &years, &library.name, &libraries);
}
Err(_) => return render_error_page()
}
} else {
return render_error_page();
}
}
Err(_) => return render_error_page()
}
}
fn organize_sermons_by_year_month(sermons: &[crate::models::JellyfinItem]) -> HashMap<String, HashMap<String, Vec<&crate::models::JellyfinItem>>> {
let mut organized: HashMap<String, HashMap<String, Vec<&crate::models::JellyfinItem>>> = HashMap::new();
for sermon in sermons {
let parsed = parse_sermon_title(&sermon.name);
let date = if let Some(ref date_str) = parsed.date_from_title {
// Try parsing the date from title using multiple formats
chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.or_else(|_| chrono::NaiveDate::parse_from_str(date_str, "%m/%d/%Y"))
.or_else(|_| chrono::NaiveDate::parse_from_str(date_str, "%m-%d-%Y"))
.unwrap_or_else(|_| {
// If date parsing fails, use premiere date or created date as fallback
if let Some(ref premiere_date) = sermon.premiere_date {
chrono::NaiveDate::parse_from_str(&premiere_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else if let Some(ref created_date) = sermon.date_created {
chrono::NaiveDate::parse_from_str(&created_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else {
chrono::Utc::now().naive_utc().date()
}
})
} else if let Some(ref premiere_date) = sermon.premiere_date {
chrono::NaiveDate::parse_from_str(&premiere_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else if let Some(ref created_date) = sermon.date_created {
chrono::NaiveDate::parse_from_str(&created_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else {
chrono::Utc::now().naive_utc().date()
};
let year = date.year().to_string();
let month = date.format("%B").to_string();
organized
.entry(year)
.or_insert_with(HashMap::new)
.entry(month)
.or_insert_with(Vec::new)
.push(sermon);
}
organized
}
fn render_archive_page(
organized: &HashMap<String, HashMap<String, Vec<&crate::models::JellyfinItem>>>,
years: &[String],
selected_collection: &str,
libraries: &[crate::models::JellyfinLibrary]
) -> Html<String> {
let collection_display_name = if selected_collection == "LiveStreams" {
"Live Stream Recordings"
} else {
"Sabbath Sermons"
};
let content = format!(r#"
<!-- Archive Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<a href="/sermons" style="color: rgba(255,255,255,0.8); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500;">
<i class="fas fa-arrow-left"></i> Back to Latest Sermons
</a>
<h1 class="section-title serif" style="color: white;">{} Archive</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Browse the complete collection organized by year and month.</p>
<!-- Collection Navigation -->
<div style="display: flex; gap: 1rem; margin-top: 2rem; flex-wrap: wrap; justify-content: center;">
{}
</div>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
{}
</div>
</section>
<script>
function toggleYear(yearId) {{
const content = document.getElementById(yearId);
const chevron = document.getElementById('chevron-' + yearId);
if (content.style.display === 'none' || content.style.display === '') {{
content.style.display = 'block';
chevron.style.transform = 'rotate(180deg)';
}} else {{
content.style.display = 'none';
chevron.style.transform = 'rotate(0deg)';
}}
}}
function toggleMonth(monthId) {{
const content = document.getElementById(monthId);
const chevron = document.getElementById('chevron-' + monthId);
if (content.style.display === 'none' || content.style.display === '') {{
content.style.display = 'block';
chevron.style.transform = 'rotate(180deg)';
}} else {{
content.style.display = 'none';
chevron.style.transform = 'rotate(0deg)';
}}
}}
// Auto-expand current year
const currentYear = new Date().getFullYear();
const currentYearElement = document.getElementById('year-' + currentYear);
if (currentYearElement) {{
toggleYear('year-' + currentYear);
}}
</script>
"#,
collection_display_name,
libraries.iter().map(|lib| {
let display_name = if lib.name == "LiveStreams" { "Live Streams" } else { "Sermons" };
let icon = if lib.name == "LiveStreams" { "broadcast-tower" } else { "church" };
let active_class = if lib.name == selected_collection { " active" } else { "" };
format!(r#"<a href="/sermons/archive?collection={}" class="btn-2025{}" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-{}"></i> {}
</a>"#, lib.name, if active_class.is_empty() { " btn-outline" } else { " btn-primary" }, icon, display_name)
}).collect::<Vec<_>>().join(""),
if years.is_empty() {
format!(r#"<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-archive"></i>
</div>
<h3 class="card-title">No {} Found</h3>
<p class="card-text">This collection doesn't contain any items yet. Please check back later.</p>
</div>"#, collection_display_name)
} else {
years.iter().map(|year| {
let year_data = organized.get(year).unwrap();
let mut months: Vec<&String> = year_data.keys().collect();
months.sort_by(|a, b| {
let month_a = chrono::NaiveDate::parse_from_str(&format!("{} 1, 2020", a), "%B %d, %Y").unwrap().month();
let month_b = chrono::NaiveDate::parse_from_str(&format!("{} 1, 2020", b), "%B %d, %Y").unwrap().month();
month_b.cmp(&month_a)
});
let total_items: usize = year_data.values().map(|sermons| sermons.len()).sum();
format!(r#"
<div class="year-section scroll-reveal" style="margin-bottom: 3rem; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<div class="year-header" style="background: var(--deep-navy); color: white; padding: 1.5rem 2rem; display: flex; align-items: center; justify-content: space-between; cursor: pointer;" onclick="toggleYear('year-{}')">
<h2 style="margin: 0; font-family: 'Playfair Display', serif; font-size: 2rem; color: white;">
<i class="fas fa-calendar-alt" style="margin-right: 1rem; color: var(--soft-gold);"></i>{}
</h2>
<div style="display: flex; align-items: center; gap: 1rem;">
<span style="background: var(--soft-gold); color: var(--deep-navy); padding: 0.5rem 1rem; border-radius: 20px; font-weight: 600;">{} items</span>
<i class="fas fa-chevron-down" id="chevron-year-{}" style="color: var(--soft-gold); font-size: 1.2rem; transition: transform 0.3s ease;"></i>
</div>
</div>
<div class="year-content" id="year-{}" style="background: white; border: 1px solid #e5e7eb; border-top: none; display: none;">
{}
</div>
</div>
"#, year, year, total_items, year, year,
months.iter().map(|month| {
let month_sermons = year_data.get(*month).unwrap();
let month_id = format!("{}-{}", year, month.replace(" ", ""));
format!(r#"
<div class="month-section" style="border-bottom: 1px solid #f3f4f6;">
<div class="month-header" style="background: var(--soft-gray); padding: 1.25rem 2rem; display: flex; align-items: center; justify-content: space-between; cursor: pointer;" onclick="toggleMonth('month-{}')">
<h3 style="margin: 0; color: var(--deep-navy); font-size: 1.25rem; font-weight: 600;">
<i class="fas fa-folder" style="margin-right: 0.75rem; color: var(--soft-gold);"></i>
{} {}
</h3>
<div style="display: flex; align-items: center; gap: 1rem;">
<span style="background: white; color: var(--deep-navy); padding: 0.25rem 0.75rem; border-radius: 12px; font-weight: 500;">{} item{}</span>
<i class="fas fa-chevron-down" id="chevron-month-{}" style="color: var(--deep-navy); transition: transform 0.3s ease;"></i>
</div>
</div>
<div class="month-content" id="month-{}" style="padding: 1.5rem 2rem; display: none;">
<div style="display: grid; gap: 1.5rem;">
{}
</div>
</div>
</div>
"#, month_id, month, year, month_sermons.len(), if month_sermons.len() == 1 { "" } else { "s" }, month_id, month_id,
month_sermons.iter().map(|sermon| {
let parsed = parse_sermon_title(&sermon.name);
let premiere_date = sermon.premiere_date.as_ref().map(|d| format_date(d)).unwrap_or_default();
let default_media_type = "Video".to_string();
let media_type = sermon.media_type.as_ref().unwrap_or(&default_media_type);
format!(r#"
<div style="display: flex; align-items: center; justify-content: space-between; padding: 1.25rem; background: var(--soft-gray); border-radius: 12px; transition: all 0.3s ease; cursor: pointer;" onclick="window.open('/sermons/{}', '_blank')" onmouseover="this.style.background='#f8fafc'; this.style.transform='translateY(-2px)'" onmouseout="this.style.background='var(--soft-gray)'; this.style.transform='translateY(0)'">
<div style="flex-grow: 1;">
<h4 style="margin: 0 0 0.5rem 0; color: var(--deep-navy); font-size: 1.1rem; font-weight: 600;">{}</h4>
<div style="display: flex; gap: 1.5rem; align-items: center; flex-wrap: wrap;">
{}
{}
<span style="background: var(--soft-gold); color: var(--deep-navy); padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; font-weight: 500;">
<i class="fas fa-{}"></i> {}
</span>
</div>
</div>
<div style="margin-left: 1rem;">
<i class="fas fa-play-circle" style="color: var(--deep-navy); font-size: 2rem;"></i>
</div>
</div>
"#, sermon.id, parsed.title,
if let Some(speaker) = parsed.speaker {
format!(r#"<span style="color: var(--medium-gray); display: flex; align-items: center; gap: 0.5rem;"><i class="fas fa-user" style="color: var(--soft-gold);"></i>{}</span>"#, speaker)
} else { String::new() },
if !premiere_date.is_empty() {
format!(r#"<span style="color: var(--medium-gray); display: flex; align-items: center; gap: 0.5rem;"><i class="fas fa-calendar" style="color: var(--soft-gold);"></i>{}</span>"#, premiere_date)
} else { String::new() },
if media_type == "Audio" { "music" } else { "video" },
media_type)
}).collect::<Vec<_>>().join(""))
}).collect::<Vec<_>>().join(""))
}).collect::<Vec<_>>().join("")
});
Html(layout(&content, &format!("{} Archive", collection_display_name)))
}
fn render_error_page() -> Html<String> {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3 class="card-title">Archive Unavailable</h3>
<p class="card-text">Unable to load sermon archive at this time. Please check back later or contact us for assistance.</p>
<a href="/sermons" class="btn-2025 btn-primary" style="margin-top: 2rem;"> Back to Latest Sermons</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Sermon Archive"))
}

View file

@ -1,167 +0,0 @@
pub fn layout(children: &str, title: &str) -> String {
format!(r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{} - Rockville Tolland SDA Church</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="/css/custom.css">
<link rel="dns-prefetch" href="https://api.rockvilletollandsda.church">
<meta name="description" content="Rockville Tolland SDA Church - Proclaiming the Three Angels' Messages with Love and Hope">
</head>
<body>
<!-- Modern 2025 Navigation -->
<nav class="nav-2025">
<div class="nav-container">
<a href="/" class="nav-brand">
<i class="fas fa-church"></i>
Rockville Tolland SDA
</a>
<ul class="nav-menu" id="nav-menu">
<li><a href="/" class="nav-link">Home</a></li>
<li><a href="/about" class="nav-link">About</a></li>
<li><a href="/ministries" class="nav-link">Ministries</a></li>
<li><a href="/sermons" class="nav-link">Sermons</a></li>
<li><a href="/events" class="nav-link">Events</a></li>
<li><a href="/bulletins" class="nav-link">Bulletins</a></li>
<li><a href="/contact" class="nav-link">Contact</a></li>
<li><a href="https://stream.rockvilletollandsda.church" target="_blank" class="nav-link" style="color: var(--soft-gold);">
<i class="fas fa-video" style="margin-right: 0.5rem;"></i>Live Stream
</a></li>
<li><a href="https://adventistgiving.org/donate/AN4MJG" target="_blank" class="nav-link" style="background: var(--gradient-gold); color: var(--midnight); border-radius: 8px;">
<i class="fas fa-heart" style="margin-right: 0.5rem;"></i>Online Giving
</a></li>
</ul>
<button class="nav-toggle" id="nav-toggle" aria-label="Toggle menu">
<span class="hamburger"></span>
<span class="hamburger"></span>
<span class="hamburger"></span>
</button>
</div>
</nav>
<!-- Main Content -->
<main>
{}
</main>
<!-- Modern Footer -->
<footer class="footer-2025">
<div class="container-2025">
<div style="text-align: center; position: relative; z-index: 2;">
<h3 class="serif" style="font-size: 2rem; color: var(--soft-gold); margin-bottom: 1rem;">
Rockville Tolland SDA Church
</h3>
<p style="font-size: 1.1rem; margin-bottom: 2rem; opacity: 0.9;">
Proclaiming the Three Angels' Messages with Love and Hope
</p>
<div style="display: flex; justify-content: center; gap: 3rem; margin-bottom: 3rem; flex-wrap: wrap;">
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--midnight); font-size: 1.5rem;">
<i class="fas fa-cross"></i>
</div>
<h4 style="color: var(--soft-gold); margin-bottom: 0.5rem;">Faith</h4>
<p style="opacity: 0.8; font-size: 0.9rem;">Grounded in Scripture</p>
</div>
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--midnight); font-size: 1.5rem;">
<i class="fas fa-heart"></i>
</div>
<h4 style="color: var(--soft-gold); margin-bottom: 0.5rem;">Hope</h4>
<p style="opacity: 0.8; font-size: 0.9rem;">In Christ's Return</p>
</div>
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--midnight); font-size: 1.5rem;">
<i class="fas fa-hands-helping"></i>
</div>
<h4 style="color: var(--soft-gold); margin-bottom: 0.5rem;">Love</h4>
<p style="opacity: 0.8; font-size: 0.9rem;">Through Service</p>
</div>
</div>
<div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 2rem; opacity: 0.7;">
<p>&copy; 2025 Rockville Tolland SDA Church. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<!-- Optimized JavaScript for Performance -->
<script>
// Performance-optimized scroll reveal
const observerOptions = {{
threshold: 0.05,
rootMargin: '50px 0px -20px 0px'
}};
const observer = new IntersectionObserver((entries) => {{
// Use requestAnimationFrame for smooth animations
requestAnimationFrame(() => {{
entries.forEach(entry => {{
if (entry.isIntersecting) {{
entry.target.classList.add('revealed');
// Unobserve after revealing to save resources
observer.unobserve(entry.target);
}}
}});
}});
}}, observerOptions);
// Debounced initialization for better performance
let initTimeout;
function initializeScrollReveal() {{
clearTimeout(initTimeout);
initTimeout = setTimeout(() => {{
const elements = document.querySelectorAll('.scroll-reveal:not(.revealed)');
elements.forEach(el => observer.observe(el));
}}, 16); // ~60fps
}}
document.addEventListener('DOMContentLoaded', () => {{
// Check if user prefers reduced motion
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {{
// Skip animations for users who prefer reduced motion
document.querySelectorAll('.scroll-reveal').forEach(el => {{
el.classList.add('revealed');
}});
return;
}}
// Initialize scroll reveal with performance optimization
initializeScrollReveal();
// Mobile menu toggle with passive listeners
const navToggle = document.getElementById('nav-toggle');
const navMenu = document.getElementById('nav-menu');
if (navToggle && navMenu) {{
navToggle.addEventListener('click', (e) => {{
e.preventDefault();
navMenu.classList.toggle('active');
navToggle.classList.toggle('active');
}}, {{ passive: false }});
// Close menu on outside click
document.addEventListener('click', (e) => {{
if (!navToggle.contains(e.target) && !navMenu.contains(e.target)) {{
navMenu.classList.remove('active');
navToggle.classList.remove('active');
}}
}}, {{ passive: true }});
}}
}});
</script>
</body>
</html>
"#, title, children)
}

View file

@ -1,69 +0,0 @@
use axum::{
routing::{get, post},
Router,
};
use std::net::SocketAddr;
use tower_http::{services::ServeDir, cors::CorsLayer};
use tracing::info;
use tracing_subscriber::fmt::init;
mod handlers;
mod models;
mod services;
mod layout;
use handlers::*;
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
init();
let app = Router::new()
.route("/", get(home::home_handler))
.route("/about", get(about::about_handler))
.route("/ministries", get(ministries::ministries_handler))
.route("/sermons", get(sermons::sermons_handler))
.route("/sermons/:id", get(sermons::sermon_detail_handler))
.route("/sermons/archive", get(sermons::sermons_archive_handler))
.route("/events", get(events::events_handler))
.route("/events/upcoming", get(events::upcoming_events_handler))
.route("/events/:id", get(events::event_detail_handler))
.route("/bulletins", get(bulletins::bulletins_handler))
.route("/bulletins/:id", get(bulletins::bulletin_detail_handler))
.route("/contact", get(contact::contact_handler))
.route("/contact", post(contact::contact_form_handler))
.route("/debug/api", get(|| async {
use crate::services::ApiService;
let api = ApiService::new();
let (config, events, bulletins) = tokio::join!(
api.get_config(),
api.get_events(None), // Get all events
api.get_bulletins()
);
format!("Config: {:?}\nEvents: {:?}\nBulletins: {} items",
config.is_ok(),
match &events {
Ok(events) => format!("OK - {} items: {:?}", events.len(), events.iter().map(|e| &e.title).collect::<Vec<_>>()),
Err(e) => format!("ERROR: {}", e)
},
bulletins.as_ref().map(|b| b.len()).unwrap_or(0)
)
}))
.nest_service("/css", ServeDir::new("css").precompressed_gzip())
.nest_service("/js", ServeDir::new("js").precompressed_gzip())
.nest_service("/images", ServeDir::new("images"))
.layer(
CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_headers(tower_http::cors::Any)
);
let addr = SocketAddr::from(([0, 0, 0, 0], 3001));
info!("Server running on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}

View file

@ -1,220 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ChurchConfig {
pub church_name: Option<String>,
pub church_address: Option<String>,
pub po_box: Option<String>,
pub contact_phone: Option<String>,
pub google_maps_url: Option<String>,
pub about_text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: String,
pub title: String,
pub description: Option<String>,
pub start_time: String,
pub end_time: String,
pub location: String,
pub location_url: Option<String>,
pub category: Option<String>,
pub image: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bulletin {
pub id: String,
pub title: String,
pub date: String,
pub scripture_reading: Option<String>,
pub sabbath_school: Option<String>,
pub divine_worship: Option<String>,
pub sunset: Option<String>,
pub pdf_path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BibleVerse {
pub text: String,
pub reference: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Sermon {
pub id: String,
pub title: String,
pub speaker: Option<String>,
pub date: String,
pub description: Option<String>,
pub audio_url: Option<String>,
pub video_url: Option<String>,
pub series: Option<String>,
pub scripture: Option<String>,
pub duration: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ContactForm {
pub first_name: String,
pub last_name: Option<String>,
pub email: String,
#[serde(default)]
pub phone: Option<String>,
pub subject: String,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinAuth {
#[serde(rename = "Username")]
pub username: String,
#[serde(rename = "Pw")]
pub pw: String,
#[serde(rename = "request")]
pub request: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinAuthResponse {
#[serde(rename = "AccessToken")]
pub access_token: String,
#[serde(rename = "User")]
pub user: JellyfinUser,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinUser {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "Name")]
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinItem {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "PremiereDate")]
pub premiere_date: Option<String>,
#[serde(rename = "DateCreated")]
pub date_created: Option<String>,
#[serde(rename = "MediaType")]
pub media_type: Option<String>,
#[serde(rename = "RunTimeTicks")]
pub run_time_ticks: Option<u64>,
#[serde(rename = "Overview")]
pub overview: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinLibrary {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "Name")]
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinItemsResponse {
#[serde(rename = "Items")]
pub items: Vec<JellyfinItem>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JellyfinLibrariesResponse {
#[serde(rename = "Items")]
pub items: Vec<JellyfinLibrary>,
}
#[derive(Debug, Clone)]
pub struct ParsedSermon {
pub title: String,
pub speaker: Option<String>,
pub date_from_title: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub message: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiListResponse<T> {
pub success: bool,
pub data: ApiListData<T>,
pub message: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiListData<T> {
pub items: Vec<T>,
pub total: u32,
pub page: u32,
pub per_page: u32,
pub has_more: bool,
}
// Template-specific structs that match what templates expect
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TemplateEvent {
pub id: String,
pub title: String,
pub description: String,
pub start_time: String,
pub end_time: String,
pub location: String,
pub location_url: String,
pub category: String,
pub image: String,
}
impl From<Event> for TemplateEvent {
fn from(event: Event) -> Self {
Self {
id: event.id,
title: event.title,
description: event.description.unwrap_or_default(),
start_time: event.start_time,
end_time: event.end_time,
location: event.location,
location_url: event.location_url.unwrap_or_default(),
category: event.category.unwrap_or_default(),
image: event.image.unwrap_or_default(),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TemplateBulletin {
pub id: String,
pub title: String,
pub date: String,
pub scripture_reading: String,
pub sabbath_school: String,
pub divine_worship: String,
pub sunset: String,
pub pdf_path: String,
}
impl From<Bulletin> for TemplateBulletin {
fn from(bulletin: Bulletin) -> Self {
Self {
id: bulletin.id,
title: bulletin.title,
date: bulletin.date,
scripture_reading: bulletin.scripture_reading.unwrap_or_default(),
sabbath_school: bulletin.sabbath_school.unwrap_or_default(),
divine_worship: bulletin.divine_worship.unwrap_or_default(),
sunset: bulletin.sunset.unwrap_or_default(),
pdf_path: bulletin.pdf_path.unwrap_or_default(),
}
}
}

View file

@ -1,444 +0,0 @@
use crate::models::*;
use anyhow::Result;
use reqwest::Client;
pub struct ApiService {
client: Client,
base_url: String,
jellyfin_url: String,
jellyfin_username: String,
jellyfin_password: String,
}
impl ApiService {
pub fn new() -> Self {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(5))
.pool_idle_timeout(std::time::Duration::from_secs(90))
.pool_max_idle_per_host(10)
.build()
.unwrap_or_else(|_| Client::new());
Self {
client,
base_url: "https://api.rockvilletollandsda.church".to_string(),
jellyfin_url: std::env::var("JELLYFIN_SERVER_URL")
.unwrap_or_else(|_| "https://jellyfin.rockvilletollandsda.church".to_string()),
jellyfin_username: std::env::var("JELLYFIN_USERNAME")
.unwrap_or_else(|_| "RTSDA Mobile".to_string()),
jellyfin_password: std::env::var("JELLYFIN_PASSWORD")
.unwrap_or_else(|_| "KingofMyLife!!".to_string()),
}
}
pub async fn get_config(&self) -> Result<Option<ChurchConfig>> {
let response = self
.client
.get(&format!("{}/api/config", self.base_url))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<ChurchConfig> = response.json().await?;
Ok(api_response.data)
} else {
Ok(None)
}
}
pub async fn get_events(&self, limit: Option<u32>) -> Result<Vec<Event>> {
let mut url = format!("{}/api/events/upcoming", self.base_url);
if let Some(limit) = limit {
url.push_str(&format!("?limit={}", limit));
}
let response = self
.client
.get(&url)
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<Vec<Event>> = response.json().await?;
Ok(api_response.data.unwrap_or_default())
} else {
Ok(vec![])
}
}
pub async fn get_event(&self, id: &str) -> Result<Option<Event>> {
let response = self
.client
.get(&format!("{}/api/events/{}", self.base_url, id))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<Event> = response.json().await?;
Ok(api_response.data)
} else {
Ok(None)
}
}
pub async fn get_bulletins(&self) -> Result<Vec<Bulletin>> {
let response = self
.client
.get(&format!("{}/api/bulletins", self.base_url))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiListResponse<Bulletin> = response.json().await?;
Ok(api_response.data.items)
} else {
Ok(vec![])
}
}
pub async fn get_current_bulletin(&self) -> Result<Option<Bulletin>> {
let response = self
.client
.get(&format!("{}/api/bulletins/current", self.base_url))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<Bulletin> = response.json().await?;
Ok(api_response.data)
} else {
Ok(None)
}
}
pub async fn get_bulletin(&self, id: &str) -> Result<Option<Bulletin>> {
let response = self
.client
.get(&format!("{}/api/bulletins/{}", self.base_url, id))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<Bulletin> = response.json().await?;
Ok(api_response.data)
} else {
Ok(None)
}
}
pub async fn get_random_verse(&self) -> Result<Option<BibleVerse>> {
let response = self
.client
.get(&format!("{}/api/bible_verses/random", self.base_url))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<BibleVerse> = response.json().await?;
Ok(api_response.data)
} else {
Ok(None)
}
}
pub async fn submit_contact_form(&self, form: &ContactForm) -> Result<bool> {
let response = self
.client
.post(&format!("{}/api/contact", self.base_url))
.header("User-Agent", "RTSDA-Website/1.0")
.json(form)
.send()
.await?;
Ok(response.status().is_success())
}
#[allow(dead_code)]
pub async fn get_sermons(&self, limit: Option<u32>) -> Result<Vec<Sermon>> {
let mut url = format!("{}/api/sermons", self.base_url);
if let Some(limit) = limit {
url.push_str(&format!("?limit={}", limit));
}
let response = self
.client
.get(&url)
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiListResponse<Sermon> = response.json().await?;
Ok(api_response.data.items)
} else {
Ok(vec![])
}
}
#[allow(dead_code)]
pub async fn get_sermon(&self, id: &str) -> Result<Option<Sermon>> {
let response = self
.client
.get(&format!("{}/api/sermons/{}", self.base_url, id))
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiResponse<Sermon> = response.json().await?;
Ok(api_response.data)
} else {
Ok(None)
}
}
#[allow(dead_code)]
pub async fn get_recent_sermons(&self, limit: Option<u32>) -> Result<Vec<Sermon>> {
let mut url = format!("{}/api/sermons/recent", self.base_url);
if let Some(limit) = limit {
url.push_str(&format!("?limit={}", limit));
}
let response = self
.client
.get(&url)
.header("User-Agent", "RTSDA-Website/1.0")
.send()
.await?;
if response.status().is_success() {
let api_response: ApiListResponse<Sermon> = response.json().await?;
Ok(api_response.data.items)
} else {
Ok(vec![])
}
}
// Jellyfin integration methods
pub async fn authenticate_jellyfin(&self) -> Result<Option<(String, String)>> {
let auth_request = JellyfinAuth {
username: self.jellyfin_username.clone(),
pw: self.jellyfin_password.clone(),
request: serde_json::Value::Object(serde_json::Map::new()),
};
let response = self
.client
.post(&format!("{}/Users/authenticatebyname", self.jellyfin_url))
.header("Content-Type", "application/json")
.header("X-Emby-Authorization", r#"MediaBrowser Client="RTSDA Church Website", Device="Web", DeviceId="church-website", Version="1.0.0""#)
.json(&auth_request)
.send()
.await?;
if response.status().is_success() {
let auth_response: JellyfinAuthResponse = response.json().await?;
Ok(Some((auth_response.access_token, auth_response.user.id)))
} else {
Ok(None)
}
}
pub async fn get_jellyfin_libraries(&self) -> Result<Vec<JellyfinLibrary>> {
let auth = self.authenticate_jellyfin().await?;
if let Some((token, user_id)) = auth {
let response = self
.client
.get(&format!("{}/Users/{}/Views", self.jellyfin_url, user_id))
.header("X-Emby-Authorization", format!(r#"MediaBrowser Token="{}""#, token))
.send()
.await?;
if response.status().is_success() {
let libraries_response: JellyfinLibrariesResponse = response.json().await?;
let sermon_libraries = libraries_response.items.into_iter()
.filter(|lib| lib.name == "Sermons" || lib.name == "LiveStreams")
.collect();
Ok(sermon_libraries)
} else {
Ok(vec![])
}
} else {
Ok(vec![])
}
}
pub async fn get_jellyfin_sermons(&self, parent_id: Option<&str>, limit: Option<u32>) -> Result<Vec<JellyfinItem>> {
let auth = self.authenticate_jellyfin().await?;
if let Some((token, user_id)) = auth {
let mut params = vec![
("UserId".to_string(), user_id.clone()),
("Recursive".to_string(), "true".to_string()),
("IncludeItemTypes".to_string(), "Movie,Audio,Video".to_string()),
("SortBy".to_string(), "DateCreated".to_string()),
("SortOrder".to_string(), "Descending".to_string()),
];
if let Some(l) = limit {
params.push(("Limit".to_string(), l.to_string()));
}
if let Some(pid) = parent_id {
params.push(("ParentId".to_string(), pid.to_string()));
}
let url_params: String = params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let response = self
.client
.get(&format!("{}/Users/{}/Items?{}", self.jellyfin_url, user_id, url_params))
.header("X-Emby-Authorization", format!(r#"MediaBrowser Token="{}""#, token))
.send()
.await?;
if response.status().is_success() {
let items_response: JellyfinItemsResponse = response.json().await?;
Ok(items_response.items)
} else {
Ok(vec![])
}
} else {
Ok(vec![])
}
}
pub async fn get_jellyfin_sermon(&self, sermon_id: &str) -> Result<Option<JellyfinItem>> {
let auth = self.authenticate_jellyfin().await?;
if let Some((token, user_id)) = auth {
let response = self
.client
.get(&format!("{}/Users/{}/Items/{}", self.jellyfin_url, user_id, sermon_id))
.header("X-Emby-Authorization", format!(r#"MediaBrowser Token="{}""#, token))
.send()
.await?;
if response.status().is_success() {
let sermon: JellyfinItem = response.json().await?;
Ok(Some(sermon))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
pub fn get_jellyfin_stream_url(&self, sermon_id: &str, token: &str) -> String {
format!("{}/Videos/{}/stream?api_key={}&static=true", self.jellyfin_url, sermon_id, token)
}
}
pub fn format_event_datetime(start_time: &str, end_time: &str) -> String {
use chrono::DateTime;
// Parse the datetime strings
if let (Ok(start), Ok(end)) = (
DateTime::parse_from_rfc3339(start_time).or_else(|_| DateTime::parse_from_str(start_time, "%Y-%m-%dT%H:%M:%S%.fZ")),
DateTime::parse_from_rfc3339(end_time).or_else(|_| DateTime::parse_from_str(end_time, "%Y-%m-%dT%H:%M:%S%.fZ"))
) {
let start_date = start.format("%a, %b %d").to_string();
let start_time_str = start.format("%l:%M %p").to_string().trim().to_string();
let end_time_str = end.format("%l:%M %p").to_string().trim().to_string();
// Check if it's an all-day event (starts at midnight)
if start.format("%H:%M:%S").to_string() == "00:00:00" &&
end.format("%H:%M:%S").to_string() == "00:00:00" {
return format!("{} - All Day", start_date);
}
// Check if same day
if start.date_naive() == end.date_naive() {
format!("{} at {} - {}", start_date, start_time_str, end_time_str)
} else {
let end_date = end.format("%a, %b %d").to_string();
format!("{} {} - {} {}", start_date, start_time_str, end_date, end_time_str)
}
} else {
// Fallback to simple formatting
format!("{} - {}", start_time, end_time)
}
}
pub fn strip_html(html: &str) -> String {
// Simple HTML stripping and decode basic HTML entities
let mut result = html.to_string();
// Simple HTML tag removal (basic implementation)
while let Some(start) = result.find('<') {
if let Some(end) = result.find('>') {
if end > start {
result.replace_range(start..=end, "");
} else {
break;
}
} else {
break;
}
}
result
.replace("&nbsp;", " ")
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.trim()
.to_string()
}
pub fn parse_sermon_title(full_title: &str) -> ParsedSermon {
// Parse format: "Title - Speaker | Date"
let parts: Vec<&str> = full_title.split(" | ").collect();
let title_and_speaker = parts[0];
let date_from_title = if parts.len() > 1 { Some(parts[1].to_string()) } else { None };
let speaker_parts: Vec<&str> = title_and_speaker.split(" - ").collect();
let title = speaker_parts[0].trim().to_string();
let speaker = if speaker_parts.len() > 1 {
Some(speaker_parts[1].trim().to_string())
} else {
None
};
ParsedSermon {
title,
speaker,
date_from_title,
}
}
pub fn format_duration(ticks: u64) -> String {
let total_seconds = ticks / 10_000_000;
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
if hours > 0 {
format!("{}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{}:{:02}", minutes, seconds)
}
}
pub fn format_date(date_string: &str) -> String {
use chrono::NaiveDate;
// For datetime strings, extract just the date part to avoid timezone conversion
let date_only = date_string.split('T').next().unwrap_or(date_string);
if let Ok(date) = NaiveDate::parse_from_str(date_only, "%Y-%m-%d") {
date.format("%B %d, %Y").to_string()
} else {
date_string.to_string()
}
}

View file

@ -1,290 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<!-- About Us Header -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white; font-size: clamp(3rem, 6vw, 5rem);">About Our Church</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9); font-size: 1.3rem; max-width: 800px;">
Founded on Biblical principles and committed to proclaiming the Three Angels' Messages, we are a community of believers dedicated to worship, fellowship, and service.
</p>
</div>
</div>
</section>
<!-- Our Mission & History -->
<section class="section-2025">
<div class="container-2025">
<div class="cards-grid" style="margin-top: 0;">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-heart"></i>
</div>
<h3 class="card-title">Our Mission</h3>
<p class="card-text">
To proclaim the everlasting gospel of Jesus Christ in the context of the Three Angels' Messages of Revelation 14:6-12,
leading people to accept Jesus as their personal Savior and unite with His remnant church, discipling them to serve Him as Lord
and preparing them for His soon return.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-church"></i>
</div>
<h3 class="card-title">Our Heritage</h3>
<p class="card-text">
As part of the Seventh-day Adventist Church, we trace our roots to the great Second Advent awakening of the 1840s.
We are heirs of the Protestant Reformation and hold to the Bible as our only rule of faith and practice.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-hands-helping"></i>
</div>
<h3 class="card-title">Our Community</h3>
<p class="card-text">
We believe in fostering a welcoming community where every person can experience God's love, grow in faith,
and discover their unique gifts for ministry. Together, we serve our local community and support global mission.
</p>
</div>
</div>
</div>
</section>
<!-- Core Beliefs -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Our Core Beliefs</h2>
<p class="section-subtitle">
Seventh-day Adventists accept the Bible as their only creed and hold certain fundamental beliefs to be the teaching of the Holy Scriptures.
</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-bible"></i>
</div>
<h4 class="card-title">The Holy Scriptures</h4>
<p class="card-text">
The Holy Scriptures, Old and New Testaments, are the written Word of God, given by divine inspiration.
They are the authoritative revealer of doctrines, and the trustworthy record of God's acts in history.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-cross"></i>
</div>
<h4 class="card-title">The Trinity</h4>
<p class="card-text">
There is one God: Father, Son, and Holy Spirit, a unity of three co-eternal Persons.
God is immortal, all-powerful, all-knowing, above all, and ever present.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-dove"></i>
</div>
<h4 class="card-title">The Father</h4>
<p class="card-text">
God the eternal Father is the Creator, Source, Sustainer, and Sovereign of all creation.
He is just and holy, merciful and gracious, slow to anger, and abounding in steadfast love and faithfulness.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-heart"></i>
</div>
<h4 class="card-title">The Son</h4>
<p class="card-text">
God the eternal Son became incarnate in Jesus Christ. Through Him all things were created,
the character of God is revealed, the salvation of humanity is accomplished, and the world is judged.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-fire"></i>
</div>
<h4 class="card-title">The Holy Spirit</h4>
<p class="card-text">
God the eternal Spirit was active with the Father and the Son in Creation, incarnation, and redemption.
He inspired the writers of Scripture and filled Christ's life with power.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-user-plus"></i>
</div>
<h4 class="card-title">Creation</h4>
<p class="card-text">
God is Creator of all things, and has revealed in Scripture the authentic account of His creative activity.
In six days the Lord made "the heaven and the earth" and rested on the seventh day.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-seedling"></i>
</div>
<h4 class="card-title">Nature of Humanity</h4>
<p class="card-text">
Man and woman were made in the image of God with individuality, the power and freedom to think and to do.
Though created free beings, each is an indivisible unity of body, mind, and spirit.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h4 class="card-title">The Sabbath</h4>
<p class="card-text">
The beneficent Creator, after the six days of Creation, rested on the seventh day and instituted the Sabbath
for all people as a memorial of Creation and a sign of sanctification.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h4 class="card-title">The Great Controversy</h4>
<p class="card-text">
All humanity is now involved in a great controversy between Christ and Satan regarding the character of God,
His law, and His sovereignty over the universe.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-praying-hands"></i>
</div>
<h4 class="card-title">Life, Death, and Resurrection of Christ</h4>
<p class="card-text">
In Christ's life of perfect obedience to God's will, His suffering, death, and resurrection,
God provided the only means of atonement for human sin.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-water"></i>
</div>
<h4 class="card-title">The Experience of Salvation</h4>
<p class="card-text">
In infinite love and mercy God made Christ, who knew no sin, to be sin for us,
so that in Him we might be made the righteousness of God.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-cloud"></i>
</div>
<h4 class="card-title">The Second Coming</h4>
<p class="card-text">
The second coming of Christ is the blessed hope of the church, the grand climax of the gospel.
The Savior's coming will be literal, personal, visible, and worldwide.
</p>
</div>
</div>
</div>
</section>
<!-- Three Angels Messages -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title serif">The Three Angels' Messages</h2>
<p class="section-subtitle">
Central to our identity as Seventh-day Adventists are the messages found in Revelation 14:6-12,
which we believe are particularly relevant for our time.
</p>
</div>
<div class="angels-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-dove"></i>
</div>
<h3 class="card-title">First Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500; line-height: 1.6;">
"Then I saw another angel flying in the midst of heaven, having the everlasting gospel to preach to those who dwell on the earth—to every nation, tribe, tongue, and people—saying with a loud voice, 'Fear God and give glory to Him, for the hour of His judgment has come; and worship Him who made heaven and earth, the sea and springs of water.'"
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:6-7)</cite>
<p class="card-text" style="margin-top: 1.5rem;">
This message calls all people to worship the Creator God who made heaven and earth. It emphasizes the everlasting gospel
and announces that the hour of God's judgment has come. This is a call to recognize God's authority as Creator and give Him glory.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-bullhorn"></i>
</div>
<h3 class="card-title">Second Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500; line-height: 1.6;">
"And another angel followed, saying, 'Babylon is fallen, is fallen, that great city, because she has made all nations drink of the wine of the wrath of her fornication.'"
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:8)</cite>
<p class="card-text" style="margin-top: 1.5rem;">
This message warns about spiritual Babylon and announces its fall. It calls people to come out of false religious systems
and spiritual confusion, choosing truth over tradition and Scripture over human authority.
</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h3 class="card-title">Third Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500; line-height: 1.6;">
"Then a third angel followed them, saying with a loud voice, 'If anyone worships the beast and his image, and receives his mark on his forehead or on his hand, he himself shall also drink of the wine of the wrath of God... Here is the patience of the saints; here are those who keep the commandments of God and the faith of Jesus.'"
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:9-12)</cite>
<p class="card-text" style="margin-top: 1.5rem;">
This message identifies God's faithful people as those who keep the commandments of God and have the faith of Jesus.
It emphasizes the importance of remaining faithful to all of God's commandments, including the seventh-day Sabbath,
while maintaining faith in Jesus Christ as our Savior.
</p>
</div>
</div>
</div>
</section>
<!-- Call to Action -->
<section class="section-2025" style="background: var(--deep-navy); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title serif" style="color: white;">Join Our Church Family</h2>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9); margin-bottom: 3rem;">
We invite you to join us in worship, fellowship, and service as we grow together in faith and prepare for Christ's return.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center;">
<a href="/events" class="btn-2025 btn-primary">
<i class="fas fa-calendar-alt"></i>
Join Us This Sabbath
</a>
<a href="/contact" class="btn-2025 btn-outline">
<i class="fas fa-envelope"></i>
Get In Touch
</a>
<a href="/sermons" class="btn-2025 btn-outline">
<i class="fas fa-play"></i>
Watch Our Sermons
</a>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,65 +0,0 @@
{% extends "layout.html" %}
{% block content %}
{% if bulletin %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto;">
<h1 class="section-title">{{ bulletin.title }}</h1>
<p style="color: var(--medium-gray); font-size: 1.1rem;">{{ bulletin.date }}</p>
{% if bulletin.scripture_reading %}
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold);">
<strong>Scripture Reading:</strong><br>
{{ bulletin.scripture_reading }}
</div>
{% endif %}
{% if bulletin.sabbath_school %}
<div style="margin: 2rem 0;">
<h3>Sabbath School</h3>
{{ bulletin.sabbath_school }}
</div>
{% endif %}
{% if bulletin.divine_worship %}
<div style="margin: 2rem 0;">
<h3>Divine Worship</h3>
{{ bulletin.divine_worship }}
</div>
{% endif %}
{% if bulletin.sunset %}
<div style="margin: 2rem 0;">
<h3>Sunset</h3>
{{ bulletin.sunset }}
</div>
{% endif %}
<div style="text-align: center; margin-top: 3rem;">
{% if bulletin.pdf_path %}
<a href="{{ bulletin.pdf_path }}" target="_blank" class="btn-2025 btn-primary">
<i class="fas fa-download"></i>
Download PDF
</a>
{% endif %}
<a href="/bulletins" class="btn-2025 btn-outline">
<i class="fas fa-arrow-left"></i>
Back to Bulletins
</a>
</div>
</div>
</div>
</section>
{% else %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center;">
<h1>Bulletin Not Found</h1>
<p>The bulletin you're looking for doesn't exist.</p>
<a href="/bulletins" class="btn-2025 btn-primary">View All Bulletins</a>
</div>
</div>
</section>
{% endif %}
{% endblock %}

View file

@ -1,44 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Church Bulletins</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Weekly worship programs and announcements</p>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
{% if !bulletins.is_empty() %}
<div class="cards-grid">
{% for bulletin in bulletins %}
<div class="card-2025 scroll-reveal">
<div class="card-icon-2025">
<i class="fas fa-file-alt"></i>
</div>
<h3 class="card-title">{{ bulletin.title }}</h3>
<p style="color: var(--medium-gray);">{{ bulletin.date }}</p>
{% if bulletin.scripture_reading %}
<p style="font-style: italic; margin: 1rem 0;">{{ bulletin.scripture_reading }}</p>
{% endif %}
<div style="margin-top: 1.5rem;">
<a href="/bulletins/{{ bulletin.id }}" class="btn-2025 btn-primary">View Details</a>
{% if bulletin.pdf_path %}
<a href="{{ bulletin.pdf_path }}" target="_blank" class="btn-2025 btn-outline">Download PDF</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card-2025 scroll-reveal" style="text-align: center;">
<h3>No bulletins available</h3>
<p>Check back soon for new bulletins!</p>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -1,138 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Contact Us</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">We'd love to hear from you and answer any questions</p>
</div>
</div>
</section>
{% if success %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; background: #d4edda; border-left: 4px solid #28a745;">
<h3 style="color: #155724;">Message Sent Successfully!</h3>
<p style="color: #155724;">Thank you for your message. We'll get back to you soon.</p>
</div>
</div>
</section>
{% endif %}
{% if error %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; background: #f8d7da; border-left: 4px solid #dc3545;">
<h3 style="color: #721c24;">Error Sending Message</h3>
<p style="color: #721c24;">There was an error sending your message. Please try again later.</p>
</div>
</div>
</section>
{% endif %}
<section class="section-2025">
<div class="container-2025">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: start;">
<!-- Contact Information -->
<div class="card-2025 scroll-reveal">
<div class="card-icon-2025">
<i class="fas fa-church"></i>
</div>
<h3 class="card-title">Church Information</h3>
<h4 style="margin: 1.5rem 0 1rem;">{{ church_name }}</h4>
{% if church_address %}
<p style="margin: 0.5rem 0; display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold);"></i>
{{ church_address }}
</p>
{% endif %}
{% if po_box %}
<p style="margin: 0.5rem 0; display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-mail-bulk" style="color: var(--soft-gold);"></i>
{{ po_box }}
</p>
{% endif %}
{% if contact_phone %}
<p style="margin: 0.5rem 0; display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-phone" style="color: var(--soft-gold);"></i>
{{ contact_phone }}
</p>
{% endif %}
{% if google_maps_url %}
<div style="margin-top: 1.5rem;">
<a href="{{ google_maps_url }}" target="_blank" class="btn-2025 btn-primary">
<i class="fas fa-map"></i>
View on Google Maps
</a>
</div>
{% endif %}
<div style="margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--soft-gray);">
<h4>Service Times</h4>
<p><strong>Sabbath School:</strong> 9:30 AM</p>
<p><strong>Divine Worship:</strong> 11:00 AM</p>
<p><strong>Prayer Meeting:</strong> Wednesday 7:00 PM</p>
</div>
</div>
<!-- Contact Form -->
<div class="card-2025 scroll-reveal">
<div class="card-icon-2025">
<i class="fas fa-envelope"></i>
</div>
<h3 class="card-title">Send Us a Message</h3>
<form method="post" style="margin-top: 2rem;">
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">First Name *</label>
<input type="text" name="first_name" required
style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Last Name</label>
<input type="text" name="last_name"
style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Email Address *</label>
<input type="email" name="email" required
style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Phone Number</label>
<input type="tel" name="phone"
style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Subject *</label>
<input type="text" name="subject" required
style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem;">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600; color: var(--deep-navy);">Message *</label>
<textarea name="message" required rows="5"
style="width: 100%; padding: 0.75rem; border: 2px solid var(--soft-gray); border-radius: 8px; font-size: 1rem; resize: vertical;"></textarea>
</div>
<button type="submit" class="btn-2025 btn-primary" style="width: 100%;">
<i class="fas fa-paper-plane"></i>
Send Message
</button>
</form>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,28 +0,0 @@
{% extends "layout.html" %}
{% block content %}
{% if event %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal">
<h1 class="section-title">{{ event.title }}</h1>
<p><i class="fas fa-clock"></i> {{ event.start_time }} - {{ event.end_time }}</p>
<p><i class="fas fa-map-marker-alt"></i> {{ event.location }}</p>
{% if event.description %}
<div style="margin-top: 2rem;">{{ event.description }}</div>
{% endif %}
</div>
</div>
</section>
{% else %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center;">
<h1>Event Not Found</h1>
<p>The event you're looking for doesn't exist.</p>
<a href="/events" class="btn-2025 btn-primary">View All Events</a>
</div>
</div>
</section>
{% endif %}
{% endblock %}

View file

@ -1,39 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Upcoming Events</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Join us for worship, fellowship, and community service</p>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
{% if !upcoming_events.is_empty() %}
<div class="events-grid">
{% for event in upcoming_events %}
<div class="event-card-2025 scroll-reveal">
<div class="event-image-2025" {% if event.image %}style="background-image: url('{{ event.image }}');"{% endif %}></div>
<div class="event-content-2025">
<h3 class="event-title">{{ event.title }}</h3>
<p><i class="fas fa-clock"></i> {{ event.start_time }} - {{ event.end_time }}</p>
<p><i class="fas fa-map-marker-alt"></i> {{ event.location }}</p>
{% if event.description %}
<p>{{ event.description }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card-2025 scroll-reveal" style="text-align: center;">
<h3>No upcoming events at this time</h3>
<p>Check back soon for new events and activities!</p>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -1,358 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<!-- 2025 Hero Section -->
<section class="hero-2025">
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title serif">
Welcome to<br>
{{ church_name }}
</h1>
<p class="hero-subtitle">
{{ about_text }}
</p>
<div class="hero-cta-group">
<a href="/events" class="btn-2025 btn-primary">
<i class="fas fa-calendar-alt"></i>
Join Us This Sabbath
</a>
<a href="/sermons" class="btn-2025 btn-outline">
<i class="fas fa-play"></i>
Watch Online
</a>
</div>
<div style="display: flex; gap: 2rem; margin-top: 2rem; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 0.75rem; color: rgba(255,255,255,0.9);">
<div style="width: 12px; height: 12px; background: var(--soft-gold); border-radius: 50%;"></div>
<span style="font-size: 0.95rem;">Sabbath School 9:30 AM</span>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem; color: rgba(255,255,255,0.9);">
<div style="width: 12px; height: 12px; background: var(--soft-gold); border-radius: 50%;"></div>
<span style="font-size: 0.95rem;">Divine Worship 11:00 AM</span>
</div>
</div>
</div>
<div class="hero-visual">
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-dove" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">First Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Fear God & Give Glory</p>
</div>
</div>
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-bullhorn" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">Second Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Babylon is Fallen</p>
</div>
</div>
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-scroll" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">Third Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Keep God's Commands</p>
</div>
</div>
</div>
</div>
</section>
<!-- Three Angels Messages 2025 -->
<section class="section-2025 angels-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title serif">The Three Angels' Messages</h2>
<p class="section-subtitle">Central to our mission as Seventh-day Adventists, these messages from Revelation 14 guide our purpose and calling.</p>
</div>
<div class="angels-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-dove"></i>
</div>
<h3 class="card-title">First Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Fear God and give glory to Him, for the hour of His judgment has come; and worship Him who made heaven and earth, the sea and springs of water."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:6-7)</cite>
<p class="card-text" style="margin-top: 1.5rem;">The everlasting gospel calls all people to worship the Creator God who made heaven and earth, recognizing His authority and giving Him glory.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-bullhorn"></i>
</div>
<h3 class="card-title">Second Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Babylon is fallen, is fallen, that great city, because she has made all nations drink of the wine of the wrath of her fornication."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:8)</cite>
<p class="card-text" style="margin-top: 1.5rem;">A warning about false religious systems and a call to come out of spiritual confusion, choosing truth over tradition.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h3 class="card-title">Third Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Here is the patience of the saints; here are those who keep the commandments of God and the faith of Jesus."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:12)</cite>
<p class="card-text" style="margin-top: 1.5rem;">A call to remain faithful to God's commandments, including the seventh-day Sabbath, while maintaining faith in Jesus Christ.</p>
</div>
</div>
</div>
</section>
{% if bible_verse %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 class="section-title" style="font-size: 2rem; margin-bottom: 2rem;">Today's Scripture</h3>
<blockquote style="font-family: 'Playfair Display', serif; font-size: 1.4rem; font-style: italic; color: var(--deep-navy); margin: 2rem 0; line-height: 1.6;">
"{{ bible_verse.text }}"
</blockquote>
<cite style="color: var(--medium-gray); font-size: 1.1rem; font-weight: 600;">- {{ bible_verse.reference }}</cite>
</div>
</div>
</section>
{% endif %}
<!-- Service Times 2025 -->
<section class="section-2025 services-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title" style="color: white;">Service Times</h2>
<p class="section-subtitle" style="color: rgba(255,255,255,0.8);">Join us for worship and fellowship</p>
</div>
<div class="services-grid">
<div class="service-card-2025 scroll-reveal stagger-1">
<div class="service-icon-2025">
<i class="fas fa-book"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Sabbath School</h3>
<div class="service-time">9:30 AM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Join us for Bible study and fellowship every Sabbath morning</p>
</div>
<div class="service-card-2025 scroll-reveal stagger-2">
<div class="service-icon-2025">
<i class="fas fa-praying-hands"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Divine Worship</h3>
<div class="service-time">11:00 AM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Worship service with inspiring sermons and uplifting music</p>
</div>
<div class="service-card-2025 scroll-reveal stagger-3">
<div class="service-icon-2025">
<i class="fas fa-hands-praying"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Prayer Meeting</h3>
<div class="service-time">Wed 7:00 PM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Mid-week spiritual refreshment with prayer and Bible study</p>
</div>
</div>
</div>
</section>
{% if current_bulletin %}
<!-- This Week's Bulletin -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">This Week's Bulletin</h2>
<p class="section-subtitle">Stay informed about church activities and worship</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-file-alt"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 1rem;">{{ current_bulletin.title }}</h3>
<p style="color: var(--medium-gray); font-size: 1.1rem; margin-bottom: 2rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{{ current_bulletin.date }}
</p>
{% if current_bulletin.scripture_reading %}
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy);">
<strong style="color: var(--deep-navy);">Scripture Reading:</strong><br>
{{ current_bulletin.scripture_reading }}
</div>
{% endif %}
<div style="display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; margin-top: 2rem;">
{% if current_bulletin.pdf_path %}
<a href="{{ current_bulletin.pdf_path }}" target="_blank" class="btn-2025 btn-primary">
<i class="fas fa-download"></i>
Download PDF
</a>
{% endif %}
<a href="/bulletins/{{ current_bulletin.id }}" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-eye"></i>
View Details
</a>
<a href="/bulletins" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-archive"></i>
View Archive
</a>
</div>
</div>
</div>
</section>
{% endif %}
{% if !upcoming_events.is_empty() %}
<!-- Upcoming Events -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Upcoming Events</h2>
<p class="section-subtitle">Join us for these special occasions and activities</p>
</div>
<div class="events-grid">
{% for event in upcoming_events %}
<a href="/events/{{ event.id }}" style="text-decoration: none; color: inherit; display: block;">
<div class="event-card-2025 scroll-reveal stagger-{{ loop.index % 3 + 1 }}" style="cursor: pointer;">
<div class="event-image-2025" {% if event.image %}style="background-image: url('{{ event.image }}'); background-size: cover; background-position: center;"{% endif %}>
</div>
<div class="event-content-2025">
<h3 class="event-title" style="margin-bottom: 1.5rem;">{{ event.title }}</h3>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-clock" style="color: var(--soft-gold);"></i>
{{ event.start_time }} - {{ event.end_time }}
</p>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold);"></i>
{{ event.location }}
</p>
<div style="margin: 1.5rem 0; color: var(--medium-gray); line-height: 1.6;">
{% if event.description %}
{{ event.description }}
{% else %}
Join us for this special event.
{% endif %}
</div>
<div class="btn-2025 btn-primary" style="background: var(--gradient-gold) !important; color: var(--midnight) !important; display: inline-flex; align-items: center; gap: 0.5rem;">
View Details <i class="fas fa-arrow-right"></i>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
<div style="text-align: center; margin-top: 3rem;">
<a href="/events" class="btn-2025 btn-primary" style="font-size: 1.1rem; padding: 1rem 3rem;">
<i class="fas fa-calendar" style="margin-right: 0.5rem;"></i>
View All Events
</a>
</div>
</div>
</section>
{% endif %}
<!-- Our Beliefs 2025 -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Our Core Beliefs</h2>
<p class="section-subtitle">As Seventh-day Adventists, we accept the Bible as our only creed and hold certain fundamental beliefs to be the teaching of the Holy Scriptures.</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-bible"></i>
</div>
<h4 class="card-title">The Holy Scriptures</h4>
<p class="card-text">The Holy Scriptures are the infallible revelation of God's will and the authoritative revealer of doctrines.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-cross"></i>
</div>
<h4 class="card-title">The Trinity</h4>
<p class="card-text">There is one God: Father, Son, and Holy Spirit, a unity of three co-eternal Persons.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h4 class="card-title">The Sabbath</h4>
<p class="card-text">The seventh day of the week is the Sabbath of the Lord our God, a day of rest and worship.</p>
</div>
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-cloud"></i>
</div>
<h4 class="card-title">The Second Coming</h4>
<p class="card-text">The second coming of Christ is the blessed hope of the church and the grand climax of the gospel.</p>
</div>
</div>
</div>
</section>
<!-- Mobile App Section -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Faith in Your Pocket</h2>
<p class="section-subtitle">Access sermons, events, and stay connected with our church family through our mobile app designed for spiritual growth.</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-mobile-alt"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 2rem;">Download Our Mobile App</h3>
<p style="color: var(--medium-gray); line-height: 1.6; margin-bottom: 3rem; font-size: 1.1rem;">
Stay connected with sermons, events, and church activities wherever you go.
Our app makes it easy to access spiritual content and stay engaged with our community.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center; align-items: center;">
<!-- iOS App Store -->
<a href="https://apps.apple.com/us/app/rtsda/id6738595657" target="_blank" rel="noopener noreferrer" style="display: block; transition: transform 0.3s ease;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
<img src="/images/app-store-badge.svg" alt="Download on the App Store" style="height: 60px; border-radius: 8px;">
</a>
<!-- Android APK Download -->
<button onclick="downloadApk()" class="btn-2025 btn-primary" style="display: flex; align-items: center; gap: 1rem; font-size: 1rem; padding: 1rem 1.5rem; background: var(--deep-navy); border: none; cursor: pointer;">
<svg viewBox="0 0 24 24" style="height: 40px; width: 40px; fill: var(--soft-gold);">
<path d="M17.6 9.48l1.84-3.18c.16-.31.04-.69-.26-.85a.637.637 0 0 0-.83.22l-1.88 3.24a11.463 11.463 0 0 0-8.94 0L5.65 5.67a.643.643 0 0 0-.87-.2c-.28.18-.37.54-.22.83L6.4 9.48A10.78 10.78 0 0 0 1 18h22a10.78 10.78 0 0 0-5.4-8.52zM7 15.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5zm10 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5z"/>
</svg>
<div style="text-align: left; color: white;">
<div style="font-size: 0.8rem; opacity: 0.8;">DOWNLOAD APK</div>
<div style="font-size: 1.2rem; font-weight: 600;">Android</div>
</div>
</button>
</div>
<div style="margin-top: 2rem; padding: 1.5rem; background: rgba(255,255,255,0.8); border-radius: 12px; border-left: 4px solid var(--soft-gold);">
<p style="color: var(--deep-navy); margin: 0; font-style: italic;">
<i class="fas fa-info-circle" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
Available on both iOS and Android platforms. Download today to access sermons, events, and stay connected with our church community.
</p>
</div>
</div>
</div>
</section>
<script>
function downloadApk() {
window.location.href = 'https://api.rockvilletollandsda.church/uploads/rtsda_android/current';
}
</script>
{% endblock %}

View file

@ -1,358 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<!-- 2025 Hero Section -->
<section class="hero-2025">
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title serif">
Welcome to<br>
{{ church_name }}
</h1>
<p class="hero-subtitle">
{{ about_text }}
</p>
<div class="hero-cta-group">
<a href="/events" class="btn-2025 btn-primary">
<i class="fas fa-calendar-alt"></i>
Join Us This Sabbath
</a>
<a href="/sermons" class="btn-2025 btn-outline">
<i class="fas fa-play"></i>
Watch Online
</a>
</div>
<div style="display: flex; gap: 2rem; margin-top: 2rem; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 0.75rem; color: rgba(255,255,255,0.9);">
<div style="width: 12px; height: 12px; background: var(--soft-gold); border-radius: 50%;"></div>
<span style="font-size: 0.95rem;">Sabbath School 9:30 AM</span>
</div>
<div style="display: flex; align-items: center; gap: 0.75rem; color: rgba(255,255,255,0.9);">
<div style="width: 12px; height: 12px; background: var(--soft-gold); border-radius: 50%;"></div>
<span style="font-size: 0.95rem;">Divine Worship 11:00 AM</span>
</div>
</div>
</div>
<div class="hero-visual">
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-dove" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">First Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Fear God & Give Glory</p>
</div>
</div>
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-bullhorn" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">Second Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Babylon is Fallen</p>
</div>
</div>
<div class="floating-card">
<div style="text-align: center; color: white;">
<i class="fas fa-scroll" style="font-size: 2rem; margin-bottom: 1rem; color: var(--warm-gold);"></i>
<h4 style="margin-bottom: 0.5rem; font-weight: 600;">Third Angel</h4>
<p style="font-size: 0.9rem; opacity: 0.9;">Keep God's Commands</p>
</div>
</div>
</div>
</div>
</section>
<!-- Three Angels Messages 2025 -->
<section class="section-2025 angels-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title serif">The Three Angels' Messages</h2>
<p class="section-subtitle">Central to our mission as Seventh-day Adventists, these messages from Revelation 14 guide our purpose and calling.</p>
</div>
<div class="angels-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-dove"></i>
</div>
<h3 class="card-title">First Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Fear God and give glory to Him, for the hour of His judgment has come; and worship Him who made heaven and earth, the sea and springs of water."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:6-7)</cite>
<p class="card-text" style="margin-top: 1.5rem;">The everlasting gospel calls all people to worship the Creator God who made heaven and earth, recognizing His authority and giving Him glory.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-bullhorn"></i>
</div>
<h3 class="card-title">Second Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Babylon is fallen, is fallen, that great city, because she has made all nations drink of the wine of the wrath of her fornication."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:8)</cite>
<p class="card-text" style="margin-top: 1.5rem;">A warning about false religious systems and a call to come out of spiritual confusion, choosing truth over tradition.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h3 class="card-title">Third Angel's Message</h3>
<blockquote style="font-style: italic; color: var(--deep-navy); margin: 1.5rem 0; font-size: 1.1rem; font-weight: 500;">
"Here is the patience of the saints; here are those who keep the commandments of God and the faith of Jesus."
</blockquote>
<cite style="color: var(--medium-gray); font-size: 0.9rem;">(Revelation 14:12)</cite>
<p class="card-text" style="margin-top: 1.5rem;">A call to remain faithful to God's commandments, including the seventh-day Sabbath, while maintaining faith in Jesus Christ.</p>
</div>
</div>
</div>
</section>
{% if bible_verse %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 class="section-title" style="font-size: 2rem; margin-bottom: 2rem;">Today's Scripture</h3>
<blockquote style="font-family: 'Playfair Display', serif; font-size: 1.4rem; font-style: italic; color: var(--deep-navy); margin: 2rem 0; line-height: 1.6;">
"{{ bible_verse.text }}"
</blockquote>
<cite style="color: var(--medium-gray); font-size: 1.1rem; font-weight: 600;">- {{ bible_verse.reference }}</cite>
</div>
</div>
</section>
{% endif %}
<!-- Service Times 2025 -->
<section class="section-2025 services-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title" style="color: white;">Service Times</h2>
<p class="section-subtitle" style="color: rgba(255,255,255,0.8);">Join us for worship and fellowship</p>
</div>
<div class="services-grid">
<div class="service-card-2025 scroll-reveal stagger-1">
<div class="service-icon-2025">
<i class="fas fa-book"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Sabbath School</h3>
<div class="service-time">9:30 AM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Join us for Bible study and fellowship every Sabbath morning</p>
</div>
<div class="service-card-2025 scroll-reveal stagger-2">
<div class="service-icon-2025">
<i class="fas fa-praying-hands"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Divine Worship</h3>
<div class="service-time">11:00 AM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Worship service with inspiring sermons and uplifting music</p>
</div>
<div class="service-card-2025 scroll-reveal stagger-3">
<div class="service-icon-2025">
<i class="fas fa-hands-praying"></i>
</div>
<h3 style="color: white; font-family: 'Playfair Display', serif; font-size: 1.5rem; margin-bottom: 1rem;">Prayer Meeting</h3>
<div class="service-time">Wed 7:00 PM</div>
<p style="color: rgba(255,255,255,0.8); margin-top: 1rem;">Mid-week spiritual refreshment with prayer and Bible study</p>
</div>
</div>
</div>
</section>
{% if current_bulletin %}
<!-- This Week's Bulletin -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">This Week's Bulletin</h2>
<p class="section-subtitle">Stay informed about church activities and worship</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-file-alt"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 1rem;">{{ current_bulletin.title }}</h3>
<p style="color: var(--medium-gray); font-size: 1.1rem; margin-bottom: 2rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{{ current_bulletin.date }}
</p>
{% if current_bulletin.scripture_reading %}
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy);">
<strong style="color: var(--deep-navy);">Scripture Reading:</strong><br>
{{ current_bulletin.scripture_reading }}
</div>
{% endif %}
<div style="display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; margin-top: 2rem;">
{% if current_bulletin.pdf_path %}
<a href="{{ current_bulletin.pdf_path }}" target="_blank" class="btn-2025 btn-primary">
<i class="fas fa-download"></i>
Download PDF
</a>
{% endif %}
<a href="/bulletins/{{ current_bulletin.id }}" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-eye"></i>
View Details
</a>
<a href="/bulletins" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-archive"></i>
View Archive
</a>
</div>
</div>
</div>
</section>
{% endif %}
{% if !upcoming_events.is_empty() %}
<!-- Upcoming Events -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Upcoming Events</h2>
<p class="section-subtitle">Join us for these special occasions and activities</p>
</div>
<div class="events-grid">
{% for event in upcoming_events %}
<a href="/events/{{ event.id }}" style="text-decoration: none; color: inherit; display: block;">
<div class="event-card-2025 scroll-reveal stagger-{{ loop.index % 3 + 1 }}" style="cursor: pointer;">
<div class="event-image-2025" {% if event.image %}style="background-image: url('{{ event.image }}'); background-size: cover; background-position: center;"{% endif %}>
</div>
<div class="event-content-2025">
<h3 class="event-title" style="margin-bottom: 1.5rem;">{{ event.title }}</h3>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-clock" style="color: var(--soft-gold);"></i>
{{ event.start_time }} - {{ event.end_time }}
</p>
<p style="margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; color: var(--medium-gray);">
<i class="fas fa-map-marker-alt" style="color: var(--soft-gold);"></i>
{{ event.location }}
</p>
<div style="margin: 1.5rem 0; color: var(--medium-gray); line-height: 1.6;">
{% if event.description %}
{{ event.description }}
{% else %}
Join us for this special event.
{% endif %}
</div>
<div class="btn-2025 btn-primary" style="background: var(--gradient-gold) !important; color: var(--midnight) !important; display: inline-flex; align-items: center; gap: 0.5rem;">
View Details <i class="fas fa-arrow-right"></i>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
<div style="text-align: center; margin-top: 3rem;">
<a href="/events" class="btn-2025 btn-primary" style="font-size: 1.1rem; padding: 1rem 3rem;">
<i class="fas fa-calendar" style="margin-right: 0.5rem;"></i>
View All Events
</a>
</div>
</div>
</section>
{% endif %}
<!-- Our Beliefs 2025 -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Our Core Beliefs</h2>
<p class="section-subtitle">As Seventh-day Adventists, we accept the Bible as our only creed and hold certain fundamental beliefs to be the teaching of the Holy Scriptures.</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-bible"></i>
</div>
<h4 class="card-title">The Holy Scriptures</h4>
<p class="card-text">The Holy Scriptures are the infallible revelation of God's will and the authoritative revealer of doctrines.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-cross"></i>
</div>
<h4 class="card-title">The Trinity</h4>
<p class="card-text">There is one God: Father, Son, and Holy Spirit, a unity of three co-eternal Persons.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h4 class="card-title">The Sabbath</h4>
<p class="card-text">The seventh day of the week is the Sabbath of the Lord our God, a day of rest and worship.</p>
</div>
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-cloud"></i>
</div>
<h4 class="card-title">The Second Coming</h4>
<p class="card-text">The second coming of Christ is the blessed hope of the church and the grand climax of the gospel.</p>
</div>
</div>
</div>
</section>
<!-- Mobile App Section -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Faith in Your Pocket</h2>
<p class="section-subtitle">Access sermons, events, and stay connected with our church family through our mobile app designed for spiritual growth.</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-mobile-alt"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 2rem;">Download Our Mobile App</h3>
<p style="color: var(--medium-gray); line-height: 1.6; margin-bottom: 3rem; font-size: 1.1rem;">
Stay connected with sermons, events, and church activities wherever you go.
Our app makes it easy to access spiritual content and stay engaged with our community.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center; align-items: center;">
<!-- iOS App Store -->
<a href="https://apps.apple.com/us/app/rtsda/id6738595657" target="_blank" rel="noopener noreferrer" style="display: block; transition: transform 0.3s ease;" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
<img src="/images/app-store-badge.svg" alt="Download on the App Store" style="height: 60px; border-radius: 8px;">
</a>
<!-- Android APK Download -->
<button onclick="downloadApk()" class="btn-2025 btn-primary" style="display: flex; align-items: center; gap: 1rem; font-size: 1rem; padding: 1rem 1.5rem; background: var(--deep-navy); border: none; cursor: pointer;">
<svg viewBox="0 0 24 24" style="height: 40px; width: 40px; fill: var(--soft-gold);">
<path d="M17.6 9.48l1.84-3.18c.16-.31.04-.69-.26-.85a.637.637 0 0 0-.83.22l-1.88 3.24a11.463 11.463 0 0 0-8.94 0L5.65 5.67a.643.643 0 0 0-.87-.2c-.28.18-.37.54-.22.83L6.4 9.48A10.78 10.78 0 0 0 1 18h22a10.78 10.78 0 0 0-5.4-8.52zM7 15.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5zm10 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5z"/>
</svg>
<div style="text-align: left; color: white;">
<div style="font-size: 0.8rem; opacity: 0.8;">DOWNLOAD APK</div>
<div style="font-size: 1.2rem; font-weight: 600;">Android</div>
</div>
</button>
</div>
<div style="margin-top: 2rem; padding: 1.5rem; background: rgba(255,255,255,0.8); border-radius: 12px; border-left: 4px solid var(--soft-gold);">
<p style="color: var(--deep-navy); margin: 0; font-style: italic;">
<i class="fas fa-info-circle" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
Available on both iOS and Android platforms. Download today to access sermons, events, and stay connected with our church community.
</p>
</div>
</div>
</div>
</section>
<script>
function downloadApk() {
window.location.href = 'https://api.rockvilletollandsda.church/uploads/rtsda_android/current';
}
</script>
{% endblock %}

View file

@ -1,51 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Welcome to {{ church_name }}</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">{{ about_text }}</p>
<div style="margin-top: 2rem;">
<a href="/events" class="btn-2025 btn-primary">Join Us This Sabbath</a>
<a href="/sermons" class="btn-2025 btn-outline">Watch Online</a>
</div>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Our Core Beliefs</h2>
<p class="section-subtitle">As Seventh-day Adventists, we accept the Bible as our only creed.</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-bible"></i>
</div>
<h4 class="card-title">The Holy Scriptures</h4>
<p class="card-text">The Holy Scriptures are the infallible revelation of God's will.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-cross"></i>
</div>
<h4 class="card-title">The Trinity</h4>
<p class="card-text">There is one God: Father, Son, and Holy Spirit.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h4 class="card-title">The Sabbath</h4>
<p class="card-text">The seventh day is the Sabbath of the Lord our God.</p>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,155 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - Rockville Tolland SDA Church</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="/css/custom.css">
<meta name="description" content="Rockville Tolland SDA Church - Proclaiming the Three Angels' Messages with Love and Hope">
</head>
<body>
<!-- Modern 2025 Navigation -->
<nav class="nav-2025">
<div class="nav-container">
<a href="/" class="nav-brand">
<i class="fas fa-church"></i>
Rockville Tolland SDA
</a>
<ul class="nav-menu" id="nav-menu">
<li><a href="/" class="nav-link">Home</a></li>
<li><a href="/about" class="nav-link">About</a></li>
<li><a href="/ministries" class="nav-link">Ministries</a></li>
<li><a href="/sermons" class="nav-link">Sermons</a></li>
<li><a href="/events" class="nav-link">Events</a></li>
<li><a href="/bulletins" class="nav-link">Bulletins</a></li>
<li><a href="/contact" class="nav-link">Contact</a></li>
<li><a href="https://stream.rockvilletollandsda.church" target="_blank" class="nav-link" style="color: var(--soft-gold);">
<i class="fas fa-video" style="margin-right: 0.5rem;"></i>Live Stream
</a></li>
<li><a href="https://adventistgiving.org/donate/AN4MJG" target="_blank" class="nav-link" style="background: var(--gradient-gold); color: var(--midnight); border-radius: 8px;">
<i class="fas fa-heart" style="margin-right: 0.5rem;"></i>Online Giving
</a></li>
</ul>
<button class="nav-toggle" id="nav-toggle" aria-label="Toggle menu">
<span class="hamburger"></span>
<span class="hamburger"></span>
<span class="hamburger"></span>
</button>
</div>
</nav>
<!-- Main Content -->
<main>
{% block content %}{% endblock %}
</main>
<!-- Modern Footer -->
<footer class="footer-2025">
<div class="container-2025">
<div style="text-align: center; position: relative; z-index: 2;">
<h3 class="serif" style="font-size: 2rem; color: var(--soft-gold); margin-bottom: 1rem;">
Rockville Tolland SDA Church
</h3>
<p style="font-size: 1.1rem; margin-bottom: 2rem; opacity: 0.9;">
Proclaiming the Three Angels' Messages with Love and Hope
</p>
<div style="display: flex; justify-content: center; gap: 3rem; margin-bottom: 3rem; flex-wrap: wrap;">
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--midnight); font-size: 1.5rem;">
<i class="fas fa-cross"></i>
</div>
<h4 style="color: var(--soft-gold); margin-bottom: 0.5rem;">Faith</h4>
<p style="opacity: 0.8; font-size: 0.9rem;">Grounded in Scripture</p>
</div>
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--midnight); font-size: 1.5rem;">
<i class="fas fa-heart"></i>
</div>
<h4 style="color: var(--soft-gold); margin-bottom: 0.5rem;">Hope</h4>
<p style="opacity: 0.8; font-size: 0.9rem;">In Christ's Return</p>
</div>
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: var(--gradient-gold); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: var(--midnight); font-size: 1.5rem;">
<i class="fas fa-hands-helping"></i>
</div>
<h4 style="color: var(--soft-gold); margin-bottom: 0.5rem;">Love</h4>
<p style="opacity: 0.8; font-size: 0.9rem;">Through Service</p>
</div>
</div>
<div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 2rem; opacity: 0.7;">
<p>&copy; 2025 Rockville Tolland SDA Church. All rights reserved.</p>
</div>
</div>
</div>
</footer>
<!-- Modern JavaScript -->
<script>
// Scroll reveal animations
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
}
});
}, observerOptions);
document.addEventListener('DOMContentLoaded', () => {
// Observe scroll reveal elements
document.querySelectorAll('.scroll-reveal').forEach(el => {
observer.observe(el);
});
// Navbar scroll effect
const nav = document.querySelector('.nav-2025');
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
nav.style.background = 'rgba(10, 10, 15, 0.98)';
} else {
nav.style.background = 'rgba(10, 10, 15, 0.95)';
}
});
// Mobile menu toggle
const navToggle = document.getElementById('nav-toggle');
const navMenu = document.getElementById('nav-menu');
navToggle.addEventListener('click', () => {
navMenu.classList.toggle('active');
navToggle.classList.toggle('active');
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!nav.contains(e.target)) {
navMenu.classList.remove('active');
navToggle.classList.remove('active');
}
});
// Close mobile menu when clicking nav links
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
navMenu.classList.remove('active');
navToggle.classList.remove('active');
});
});
});
</script>
<script src="/js/main.js"></script>
</body>
</html>

View file

@ -1,254 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<!-- Ministries Header -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white; font-size: clamp(3rem, 6vw, 5rem);">Our Ministries</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9); font-size: 1.3rem; max-width: 800px;">
Discover how God is working through our church community to serve, grow, and share His love with others.
</p>
</div>
</div>
</section>
<!-- Ministries Grid -->
<section class="section-2025">
<div class="container-2025">
<div class="cards-grid" style="margin-top: 0;">
<!-- Prayer Ministry -->
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-praying-hands"></i>
</div>
<h3 class="card-title">Prayer Ministry</h3>
<p class="card-text">
Our prayer ministry is the spiritual heartbeat of our church. We believe in the power of prayer to transform lives,
heal communities, and advance God's kingdom. Join us for our weekly prayer meetings every Wednesday at 7:00 PM,
where we intercede for our church family, community needs, and global mission.
</p>
<div style="margin-top: 2rem;">
<strong style="color: var(--deep-navy);">Schedule:</strong><br>
<span style="color: var(--medium-gray);">Wednesday Prayer Meeting - 7:00 PM</span><br>
<span style="color: var(--medium-gray);">Sabbath Morning Prayer - 9:00 AM</span>
</div>
</div>
<!-- Gardening Ministry -->
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-seedling"></i>
</div>
<h3 class="card-title">Gardening Ministry</h3>
<p class="card-text">
Our gardening ministry combines our love for God's creation with practical service to our community.
We maintain a church garden that provides fresh produce for local food banks and teaches sustainable,
healthy living practices. Whether you're a seasoned gardener or just starting out, everyone is welcome to dig in!
</p>
<div style="margin-top: 2rem;">
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-video"></i>
Watch Our Garden Tour
</a>
</div>
</div>
<!-- Bible Studies -->
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-book-open"></i>
</div>
<h3 class="card-title">Bible Studies</h3>
<p class="card-text">
Deepen your understanding of God's Word through our various Bible study opportunities.
We offer both in-person and online studies covering topics from prophecy to practical Christian living.
Our studies are designed for all levels of biblical knowledge, from beginners to advanced students.
</p>
<div style="margin-top: 2rem;">
<strong style="color: var(--deep-navy);">Resources:</strong><br>
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 1rem;">
<a href="https://www.biblestudyguides.org/" target="_blank" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy); font-size: 0.9rem; padding: 0.5rem 1rem;">
<i class="fas fa-external-link-alt"></i>
Bible Study Guides
</a>
<a href="https://www.amazingfacts.org/" target="_blank" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy); font-size: 0.9rem; padding: 0.5rem 1rem;">
<i class="fas fa-external-link-alt"></i>
Amazing Facts
</a>
</div>
</div>
</div>
<!-- Adventist Youth -->
<div class="card-2025 scroll-reveal stagger-4">
<div class="card-icon-2025">
<i class="fas fa-users"></i>
</div>
<h3 class="card-title">Adventist Youth (AY)</h3>
<p class="card-text">
Our Adventist Youth program is designed to inspire young people to love Jesus, live with purpose, and serve others.
Through dynamic worship experiences, community service projects, and fellowship activities,
our youth develop strong Christian character and leadership skills.
</p>
<div style="margin-top: 2rem;">
<strong style="color: var(--deep-navy);">Activities Include:</strong><br>
<span style="color: var(--medium-gray);">• Weekly AY meetings</span><br>
<span style="color: var(--medium-gray);">• Community service projects</span><br>
<span style="color: var(--medium-gray);">• Youth camps and retreats</span><br>
<span style="color: var(--medium-gray);">• Leadership development</span>
</div>
</div>
<!-- Health Ministry -->
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-heartbeat"></i>
</div>
<h3 class="card-title">Health Ministry</h3>
<p class="card-text">
Following Christ's ministry of healing, we promote physical, mental, and spiritual wellness.
Our health ministry offers educational programs on nutrition, stress management, and natural remedies.
We believe that caring for our bodies is part of honoring God as our Creator.
</p>
<div style="margin-top: 2rem;">
<strong style="color: var(--deep-navy);">Programs Offered:</strong><br>
<span style="color: var(--medium-gray);">• Cooking classes (plant-based nutrition)</span><br>
<span style="color: var(--medium-gray);">• Health screenings</span><br>
<span style="color: var(--medium-gray);">• Stress management workshops</span><br>
<span style="color: var(--medium-gray);">• Walking groups</span>
</div>
</div>
<!-- Training & Education -->
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-graduation-cap"></i>
</div>
<h3 class="card-title">Training & Education</h3>
<p class="card-text">
We are committed to equipping our members for effective ministry and Christian living.
Our training programs cover topics such as evangelism, public speaking, Bible study methods,
and practical ministry skills. We believe every member is called to serve according to their gifts.
</p>
<div style="margin-top: 2rem;">
<strong style="color: var(--deep-navy);">Training Areas:</strong><br>
<span style="color: var(--medium-gray);">• Evangelism and witnessing</span><br>
<span style="color: var(--medium-gray);">• Bible study methods</span><br>
<span style="color: var(--medium-gray);">• Public speaking</span><br>
<span style="color: var(--medium-gray);">• Children's ministry</span>
</div>
</div>
</div>
</div>
</section>
<!-- Get Involved Section -->
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Get Involved</h2>
<p class="section-subtitle">
God has given each of us unique gifts and talents to serve His kingdom.
Discover how you can use your abilities to make a difference in our church and community.
</p>
</div>
<div class="card-2025 scroll-reveal" style="max-width: 800px; margin: 0 auto; text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-hands-helping"></i>
</div>
<h3 class="card-title" style="font-size: 2rem; margin-bottom: 2rem;">Find Your Ministry</h3>
<p style="color: var(--medium-gray); line-height: 1.6; margin-bottom: 3rem; font-size: 1.1rem;">
Whether you have a passion for prayer, teaching, music, community service, or something else entirely,
there's a place for you in our ministry team. Contact us to learn more about volunteer opportunities
and how you can use your gifts to serve others.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center;">
<a href="/contact" class="btn-2025 btn-primary">
<i class="fas fa-envelope"></i>
Contact Us About Volunteering
</a>
<a href="/events" class="btn-2025 btn-outline" style="border: 2px solid var(--deep-navy); color: var(--deep-navy);">
<i class="fas fa-calendar"></i>
See Upcoming Ministry Events
</a>
</div>
</div>
</div>
</section>
<!-- Ministry Impact -->
<section class="section-2025">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">Ministry Impact</h2>
<p class="section-subtitle">
Through God's grace and your faithful service, our ministries are making a real difference in our community and beyond.
</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1" style="text-align: center;">
<div style="font-size: 3rem; color: var(--soft-gold); margin-bottom: 1rem;">
<i class="fas fa-users"></i>
</div>
<h3 style="font-size: 2.5rem; color: var(--deep-navy); margin-bottom: 0.5rem;">150+</h3>
<h4 class="card-title">People Served Monthly</h4>
<p class="card-text">Through our various ministries, we reach over 150 people each month with God's love and practical help.</p>
</div>
<div class="card-2025 scroll-reveal stagger-2" style="text-align: center;">
<div style="font-size: 3rem; color: var(--soft-gold); margin-bottom: 1rem;">
<i class="fas fa-hands-praying"></i>
</div>
<h3 style="font-size: 2.5rem; color: var(--deep-navy); margin-bottom: 0.5rem;">24/7</h3>
<h4 class="card-title">Prayer Chain</h4>
<p class="card-text">Our prayer warriors are available around the clock to lift up your needs and concerns to our loving Father.</p>
</div>
<div class="card-2025 scroll-reveal stagger-3" style="text-align: center;">
<div style="font-size: 3rem; color: var(--soft-gold); margin-bottom: 1rem;">
<i class="fas fa-seedling"></i>
</div>
<h3 style="font-size: 2.5rem; color: var(--deep-navy); margin-bottom: 0.5rem;">500+</h3>
<h4 class="card-title">Pounds of Produce</h4>
<p class="card-text">Our garden ministry has donated over 500 pounds of fresh produce to local food banks this year.</p>
</div>
<div class="card-2025 scroll-reveal stagger-4" style="text-align: center;">
<div style="font-size: 3rem; color: var(--soft-gold); margin-bottom: 1rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 style="font-size: 2.5rem; color: var(--deep-navy); margin-bottom: 0.5rem;">12</h3>
<h4 class="card-title">Bible Studies Weekly</h4>
<p class="card-text">We conduct 12 Bible studies each week, helping people grow in their understanding of God's Word.</p>
</div>
</div>
</div>
</section>
<!-- Call to Action -->
<section class="section-2025" style="background: var(--deep-navy); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title serif" style="color: white;">Join Us in Ministry</h2>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9); margin-bottom: 3rem;">
Every member is a minister. Discover how God wants to use your unique gifts and talents to serve His kingdom.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: center;">
<a href="/contact" class="btn-2025 btn-primary">
<i class="fas fa-envelope"></i>
Contact Our Ministry Leaders
</a>
<a href="/events" class="btn-2025 btn-outline">
<i class="fas fa-calendar-alt"></i>
View Ministry Calendar
</a>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,14 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal">
<h1>Sermon Player</h1>
<p>Sermon ID: {{ sermon_id }}</p>
<p>Sermon player functionality will be implemented here.</p>
<a href="/sermons" class="btn-2025 btn-primary">Back to Sermons</a>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Sermons</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Listen to inspiring messages from God's Word</p>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-play"></i>
</div>
<h3 class="card-title">Sermon Player Coming Soon</h3>
<p>We're working on integrating our sermon player. In the meantime, you can:</p>
<div style="margin-top: 2rem;">
<a href="/sermons/archive" class="btn-2025 btn-primary">
<i class="fas fa-archive"></i>
Browse Sermon Archive
</a>
<a href="https://stream.rockvilletollandsda.church" target="_blank" class="btn-2025 btn-outline">
<i class="fas fa-video"></i>
Watch Live Stream
</a>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<section class="section-2025" style="background: var(--gradient-primary); color: white; text-align: center;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Sermon Archive</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Browse our collection of past sermons and messages</p>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-archive"></i>
</div>
<h3 class="card-title">Archive Integration Coming Soon</h3>
<p>We're working on integrating our sermon archive with Jellyfin. Check back soon!</p>
<div style="margin-top: 2rem;">
<a href="/sermons" class="btn-2025 btn-primary">
<i class="fas fa-arrow-left"></i>
Back to Sermons
</a>
</div>
</div>
</div>
</section>
{% endblock %}

1
claude
View file

@ -1 +0,0 @@
/home/rockvilleav/.claude/local/claude

View file

@ -1,96 +0,0 @@
-- Script to clean existing HTML tags from database content
-- Run this script to sanitize existing data in your database
-- Clean bulletins table
UPDATE bulletins SET
title = REGEXP_REPLACE(title, '<[^>]*>', '', 'g'),
sabbath_school = REGEXP_REPLACE(COALESCE(sabbath_school, ''), '<[^>]*>', '', 'g'),
divine_worship = REGEXP_REPLACE(COALESCE(divine_worship, ''), '<[^>]*>', '', 'g'),
scripture_reading = REGEXP_REPLACE(COALESCE(scripture_reading, ''), '<[^>]*>', '', 'g'),
sunset = REGEXP_REPLACE(COALESCE(sunset, ''), '<[^>]*>', '', 'g')
WHERE
title LIKE '%<%' OR
sabbath_school LIKE '%<%' OR
divine_worship LIKE '%<%' OR
scripture_reading LIKE '%<%' OR
sunset LIKE '%<%';
-- Clean events table
UPDATE events SET
title = REGEXP_REPLACE(title, '<[^>]*>', '', 'g'),
description = REGEXP_REPLACE(description, '<[^>]*>', '', 'g'),
location = REGEXP_REPLACE(location, '<[^>]*>', '', 'g'),
location_url = REGEXP_REPLACE(COALESCE(location_url, ''), '<[^>]*>', '', 'g'),
category = REGEXP_REPLACE(category, '<[^>]*>', '', 'g'),
recurring_type = REGEXP_REPLACE(COALESCE(recurring_type, ''), '<[^>]*>', '', 'g')
WHERE
title LIKE '%<%' OR
description LIKE '%<%' OR
location LIKE '%<%' OR
location_url LIKE '%<%' OR
category LIKE '%<%' OR
recurring_type LIKE '%<%';
-- Clean pending_events table
UPDATE pending_events SET
title = REGEXP_REPLACE(title, '<[^>]*>', '', 'g'),
description = REGEXP_REPLACE(description, '<[^>]*>', '', 'g'),
location = REGEXP_REPLACE(location, '<[^>]*>', '', 'g'),
location_url = REGEXP_REPLACE(COALESCE(location_url, ''), '<[^>]*>', '', 'g'),
category = REGEXP_REPLACE(category, '<[^>]*>', '', 'g'),
recurring_type = REGEXP_REPLACE(COALESCE(recurring_type, ''), '<[^>]*>', '', 'g'),
bulletin_week = REGEXP_REPLACE(bulletin_week, '<[^>]*>', '', 'g'),
submitter_email = REGEXP_REPLACE(COALESCE(submitter_email, ''), '<[^>]*>', '', 'g'),
admin_notes = REGEXP_REPLACE(COALESCE(admin_notes, ''), '<[^>]*>', '', 'g')
WHERE
title LIKE '%<%' OR
description LIKE '%<%' OR
location LIKE '%<%' OR
location_url LIKE '%<%' OR
category LIKE '%<%' OR
recurring_type LIKE '%<%' OR
bulletin_week LIKE '%<%' OR
submitter_email LIKE '%<%' OR
admin_notes LIKE '%<%';
-- Clean contact_submissions table
UPDATE contact_submissions SET
first_name = REGEXP_REPLACE(first_name, '<[^>]*>', '', 'g'),
last_name = REGEXP_REPLACE(last_name, '<[^>]*>', '', 'g'),
email = REGEXP_REPLACE(email, '<[^>]*>', '', 'g'),
phone = REGEXP_REPLACE(COALESCE(phone, ''), '<[^>]*>', '', 'g'),
message = REGEXP_REPLACE(message, '<[^>]*>', '', 'g')
WHERE
first_name LIKE '%<%' OR
last_name LIKE '%<%' OR
email LIKE '%<%' OR
phone LIKE '%<%' OR
message LIKE '%<%';
-- Clean church_config table
UPDATE church_config SET
church_name = REGEXP_REPLACE(church_name, '<[^>]*>', '', 'g'),
contact_email = REGEXP_REPLACE(contact_email, '<[^>]*>', '', 'g'),
contact_phone = REGEXP_REPLACE(COALESCE(contact_phone, ''), '<[^>]*>', '', 'g'),
church_address = REGEXP_REPLACE(church_address, '<[^>]*>', '', 'g'),
po_box = REGEXP_REPLACE(COALESCE(po_box, ''), '<[^>]*>', '', 'g'),
google_maps_url = REGEXP_REPLACE(COALESCE(google_maps_url, ''), '<[^>]*>', '', 'g'),
about_text = REGEXP_REPLACE(about_text, '<[^>]*>', '', 'g')
WHERE
church_name LIKE '%<%' OR
contact_email LIKE '%<%' OR
contact_phone LIKE '%<%' OR
church_address LIKE '%<%' OR
po_box LIKE '%<%' OR
google_maps_url LIKE '%<%' OR
about_text LIKE '%<%';
-- Also clean HTML entities
UPDATE bulletins SET
title = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(title, '&amp;', '&'), '&lt;', '<'), '&gt;', '>'), '&quot;', '"'), '&#39;', ''''),
sabbath_school = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(sabbath_school, ''), '&amp;', '&'), '&lt;', '<'), '&gt;', '>'), '&quot;', '"'), '&#39;', ''''),
divine_worship = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(divine_worship, ''), '&amp;', '&'), '&lt;', '<'), '&gt;', '>'), '&quot;', '"'), '&#39;', ''''),
scripture_reading = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(scripture_reading, ''), '&amp;', '&'), '&lt;', '<'), '&gt;', '>'), '&quot;', '"'), '&#39;', ''''),
sunset = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(sunset, ''), '&amp;', '&'), '&lt;', '<'), '&gt;', '>'), '&quot;', '"'), '&#39;', '''');
SELECT 'Database cleaning completed. All HTML tags and entities have been removed from existing content.' as result;

View file

@ -1,33 +0,0 @@
#!/bin/bash
echo "=== COMPREHENSIVE API TEST ==="
# 1. Test Authentication
echo "1. Testing Authentication..."
TOKEN=$(curl -s -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}' \
| jq -r '.success')
echo "Auth: $TOKEN"
# 2. Test Public Endpoints
echo "2. Testing Public Endpoints..."
curl -s https://api.rockvilletollandsda.church/api/events | jq '.success'
curl -s https://api.rockvilletollandsda.church/api/bulletins | jq '.success'
curl -s https://api.rockvilletollandsda.church/api/config | jq '.success'
# 3. Test Admin Endpoints
echo "3. Testing Admin Endpoints..."
TOKEN=$(curl -s -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}' \
| jq -r '.data.token')
curl -s -H "Authorization: Bearer $TOKEN" https://api.rockvilletollandsda.church/api/admin/events/pending | jq '.success'
curl -s -H "Authorization: Bearer $TOKEN" https://api.rockvilletollandsda.church/api/admin/config | jq '.success'
# 4. Check for any remaining placeholder text
echo "4. Checking for placeholders..."
PLACEHOLDERS=$(grep -r "implement as needed\|TODO\|Working!\|n/a\|TBA" src/ 2>/dev/null | wc -l)
echo "Placeholder count: $PLACEHOLDERS"
echo "=== TEST COMPLETE ==="

View file

@ -1 +0,0 @@
rtsda-v1.0-beta7.apk

View file

@ -1,63 +0,0 @@
#!/usr/bin/env fish
echo "🖼️ DIRECT FILE COPY + PATH UPDATE (DEBUG)"
echo "=========================================="
set API_BASE "https://api.rockvilletollandsda.church/api"
set STORAGE_PATH "/media/archive/pocketbase-temp/pocketbase/pb_data/storage/2tz9osuik53a0yh"
set OLD_PB_BASE "https://pocketbase.rockvilletollandsda.church/api"
set UPLOAD_DIR "/opt/rtsda/church-api/uploads/events"
# Get token
set AUTH_RESPONSE (curl -s -X POST $API_BASE/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}')
set JWT_TOKEN (echo $AUTH_RESPONSE | jq -r '.data.token')
echo "✅ Got token"
# Get events for matching
set NEW_EVENTS (curl -s -H "Authorization: Bearer $JWT_TOKEN" "$API_BASE/events?perPage=500")
echo $NEW_EVENTS | jq '.data.items | map({id, title})' > new_events.json
set OLD_EVENTS (curl -s "$OLD_PB_BASE/collections/events/records?perPage=500")
echo $OLD_EVENTS | jq '.items | map({id, title})' > old_events.json
# Test with just ONE event to see the actual API response
set test_dir (find $STORAGE_PATH -mindepth 1 -maxdepth 1 -type d -name '[a-z0-9]*' | head -1)
set old_id (basename $test_dir)
set image_file (find $test_dir -maxdepth 1 -name "*.webp" -type f | head -1)
set old_event (cat old_events.json | jq --arg id "$old_id" '.[] | select(.id == $id)')
set title (echo $old_event | jq -r '.title')
set new_event (cat new_events.json | jq --arg title "$title" '.[] | select(.title == $title)')
set new_id (echo $new_event | jq -r '.id')
set filename (basename $image_file)
set new_filename "$new_id-$filename"
set image_path "uploads/events/$new_filename"
echo "🧪 Testing with: $title"
echo "Image path: $image_path"
# Copy file
cp "$image_file" "$UPLOAD_DIR/$new_filename"
echo "✅ File copied"
# Test the API update with debug
echo "📤 Testing API update..."
set update_response (curl -s -w "\nHTTP_CODE:%{http_code}\nCONTENT_TYPE:%{content_type}\n" \
-X PUT \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"image\": \"$image_path\"}" \
"$API_BASE/admin/events/$new_id")
echo "RAW API RESPONSE:"
echo "$update_response"
echo ""
echo "🔍 Checking if file is accessible..."
curl -I "https://api.rockvilletollandsda.church/$image_path"
rm -f new_events.json old_events.json

View file

@ -1,79 +0,0 @@
#!/bin/bash
echo "=== FIXING BROKEN CONFIG FILE ==="
# Let's recreate the config.rs file properly
cat > src/handlers/config.rs << 'EOF'
use axum::{extract::State, response::Json};
use serde_json::Value;
use crate::error::{ApiError, Result};
use crate::models::{ApiResponse, ChurchConfig};
use crate::AppState;
pub async fn get_public_config(State(state): State<AppState>) -> Result<Json<ApiResponse<Value>>> {
let config = crate::db::config::get_config(&state.pool).await?
.ok_or_else(|| ApiError::NotFound("Church config not found".to_string()))?;
// Return only public information (no API keys)
let public_config = serde_json::json!({
"church_name": config.church_name,
"contact_email": config.contact_email,
"contact_phone": config.contact_phone,
"church_address": config.church_address,
"po_box": config.po_box,
"google_maps_url": config.google_maps_url,
"about_text": config.about_text
});
Ok(Json(ApiResponse {
success: true,
data: Some(public_config),
message: None,
}))
}
pub async fn get_admin_config(State(state): State<AppState>) -> Result<Json<ApiResponse<ChurchConfig>>> {
let config = crate::db::config::get_config(&state.pool).await?
.ok_or_else(|| ApiError::NotFound("Church config not found".to_string()))?;
Ok(Json(ApiResponse {
success: true,
data: Some(config),
message: None,
}))
}
EOF
# Remove files handler if it exists
rm -f src/handlers/files.rs
# Remove files from mod.rs if it exists
sed -i '/mod files;/d' src/handlers/mod.rs 2>/dev/null || true
# Build to verify
echo "=== Building to verify fix ==="
cargo build --release
if [ $? -eq 0 ]; then
echo "✅ Build successful!"
# Check for remaining placeholders
echo "=== Checking for remaining placeholders ==="
REMAINING=$(grep -r "implement as needed\|TODO\|Working\|TBA" src/ 2>/dev/null | wc -l)
echo "Remaining placeholders: $REMAINING"
if [ $REMAINING -eq 0 ]; then
echo "🎉 ALL PLACEHOLDERS REMOVED!"
echo "🎉 YOUR CHURCH API IS 100% COMPLETE!"
# Restart service
sudo systemctl restart church-api
echo "✅ Service restarted successfully!"
else
echo "Remaining placeholders:"
grep -r "implement as needed\|TODO\|Working\|TBA" src/ 2>/dev/null
fi
else
echo "❌ Build still failing - need to debug further"
fi

View file

@ -1,15 +0,0 @@
# Add missing imports to db/events.rs
sed -i '1i use crate::models::PaginatedResponse;' src/db/events.rs
# Add missing import to handlers/events.rs
sed -i '/use crate::models::/s/$/,PaginationParams/' src/handlers/events.rs
# Fix ApiError::Internal to ValidationError (check what exists)
grep "enum ApiError" -A 10 src/error.rs
# Fix the admin_notes type issue
sed -i 's/admin_notes: &Option<String>/admin_notes: Option<&String>/' src/db/events.rs
sed -i 's/&req.admin_notes/req.admin_notes.as_ref()/' src/db/events.rs
# Replace Internal with ValidationError
sed -i 's/ApiError::Internal/ApiError::ValidationError/g' src/db/events.rs

View file

@ -1,31 +0,0 @@
# Fix the approve handler
sed -i '/pub async fn approve(/,/^}/c\
pub async fn approve(\
Path(id): Path<Uuid>,\
State(state): State<AppState>,\
Json(req): Json<ApproveRejectRequest>,\
) -> Result<Json<ApiResponse<Event>>> {\
let event = crate::db::events::approve_pending(\&state.pool, \&id, req.admin_notes).await?;\
\
Ok(Json(ApiResponse {\
success: true,\
data: Some(event),\
message: Some("Event approved successfully".to_string()),\
}))\
}' src/handlers/events.rs
# Fix the reject handler
sed -i '/pub async fn reject(/,/^}/c\
pub async fn reject(\
Path(id): Path<Uuid>,\
State(state): State<AppState>,\
Json(req): Json<ApproveRejectRequest>,\
) -> Result<Json<ApiResponse<String>>> {\
crate::db::events::reject_pending(\&state.pool, \&id, req.admin_notes).await?;\
\
Ok(Json(ApiResponse {\
success: true,\
data: Some("Event rejected".to_string()),\
message: Some("Event rejected successfully".to_string()),\
}))\
}' src/handlers/events.rs

View file

@ -1,45 +0,0 @@
# Fix the approve handler with email
sed -i '/pub async fn approve(/,/^}/c\
pub async fn approve(\
Path(id): Path<Uuid>,\
State(state): State<AppState>,\
Json(req): Json<ApproveRejectRequest>,\
) -> Result<Json<ApiResponse<Event>>> {\
let pending_event = crate::db::events::get_pending_by_id(\&state.pool, \&id).await?\
.ok_or_else(|| ApiError::NotFound("Pending event not found".to_string()))?;\
\
let event = crate::db::events::approve_pending(\&state.pool, \&id, req.admin_notes.clone()).await?;\
\
if let Some(_submitter_email) = \&pending_event.submitter_email {\
let _ = state.mailer.send_event_approval_notification(\&pending_event, req.admin_notes.as_deref()).await;\
}\
\
Ok(Json(ApiResponse {\
success: true,\
data: Some(event),\
message: Some("Event approved successfully".to_string()),\
}))\
}' src/handlers/events.rs
# Fix the reject handler with email
sed -i '/pub async fn reject(/,/^}/c\
pub async fn reject(\
Path(id): Path<Uuid>,\
State(state): State<AppState>,\
Json(req): Json<ApproveRejectRequest>,\
) -> Result<Json<ApiResponse<String>>> {\
let pending_event = crate::db::events::get_pending_by_id(\&state.pool, \&id).await?\
.ok_or_else(|| ApiError::NotFound("Pending event not found".to_string()))?;\
\
crate::db::events::reject_pending(\&state.pool, \&id, req.admin_notes.clone()).await?;\
\
if let Some(_submitter_email) = \&pending_event.submitter_email {\
let _ = state.mailer.send_event_rejection_notification(\&pending_event, req.admin_notes.as_deref()).await;\
}\
\
Ok(Json(ApiResponse {\
success: true,\
data: Some("Event rejected".to_string()),\
message: Some("Event rejected successfully".to_string()),\
}))\
}' src/handlers/events.rs

View file

@ -1,74 +0,0 @@
#!/usr/bin/env fish
echo "🔧 COMPLETE IMAGE_PATH FIX (v5 - SUBMISSION + UPDATES)"
echo "====================================================="
# Restore from backups first
if test -f "src/models.rs.backup2"
cp src/models.rs.backup2 src/models.rs
cp src/db/events.rs.backup2 src/db/events.rs
cp src/handlers/events.rs.backup2 src/handlers/events.rs
echo "✅ Restored from backups"
end
echo "1⃣ Adding image_path to BOTH CreateEventRequest AND SubmitEventRequest..."
# Add to CreateEventRequest
sed -i '/pub struct CreateEventRequest {/,/^}/ {
/pub recurring_type: Option<String>,/ a\
pub image_path: Option<String>,
}' src/models.rs
# Add to SubmitEventRequest
sed -i '/pub struct SubmitEventRequest {/,/^}/ {
/pub submitter_email: Option<String>,/ a\
pub image_path: Option<String>,
}' src/models.rs
echo "2⃣ Adding image_path to SubmitEventRequest initialization in handlers..."
sed -i '/let mut req = SubmitEventRequest {/,/};/ {
/submitter_email: None,/ a\
image_path: None,
}' src/handlers/events.rs
echo "3⃣ Fixing the submit_for_approval function SQL..."
# Update the INSERT statement to include image_path
sed -i 's|category, is_featured, recurring_type, bulletin_week, submitter_email|category, is_featured, recurring_type, bulletin_week, submitter_email, image_path|' src/db/events.rs
sed -i 's|VALUES (\$1, \$2, \$3, \$4, \$5, \$6, \$7, \$8, \$9, \$10, \$11)|VALUES (\$1, \$2, \$3, \$4, \$5, \$6, \$7, \$8, \$9, \$10, \$11, \$12)|' src/db/events.rs
echo "4⃣ Fixing the events table update function..."
sed -i 's|recurring_type = \$9, updated_at = NOW()|recurring_type = \$9, image_path = \$10, updated_at = NOW()|' src/db/events.rs
sed -i 's|WHERE id = \$10|WHERE id = \$11|' src/db/events.rs
sed -i '/req\.recurring_type,$/a\ req.image_path,' src/db/events.rs
echo "5⃣ Building..."
if cargo build
echo "✅ SUCCESS! Both submission and updates now support image_path."
echo ""
echo "=== What was fixed ==="
echo "✅ Added image_path to CreateEventRequest struct (for updates)"
echo "✅ Added image_path to SubmitEventRequest struct (for new submissions)"
echo "✅ Updated handlers to initialize image_path field"
echo "✅ Fixed submit_for_approval SQL to include image_path column"
echo "✅ Fixed update SQL to include image_path column"
echo ""
echo "🚀 Next steps:"
echo "1. Restart your API server"
echo "2. Run your image_path update script"
echo "3. Both new submissions AND existing event updates will handle image_path!"
else
echo "❌ Build failed. Let's debug..."
echo ""
echo "=== Current structs ==="
grep -A 15 "pub struct CreateEventRequest" src/models.rs
echo ""
grep -A 15 "pub struct SubmitEventRequest" src/models.rs
echo ""
echo "=== Current submit_for_approval function ==="
grep -A 15 "submit_for_approval.*SubmitEventRequest" src/db/events.rs
# Restore
cp src/models.rs.backup2 src/models.rs
cp src/db/events.rs.backup2 src/db/events.rs
cp src/handlers/events.rs.backup2 src/handlers/events.rs
echo "🔄 Restored backups"
end

View file

@ -1,125 +0,0 @@
#!/usr/bin/env fish
echo "🖼️ MIGRATING ALL IMAGE FORMATS (JPEG, PNG, etc.)"
echo "================================================"
set API_BASE "https://api.rockvilletollandsda.church/api"
set STORAGE_PATH "/media/archive/pocketbase-temp/pocketbase/pb_data/storage/2tz9osuik53a0yh"
set OLD_PB_BASE "https://pocketbase.rockvilletollandsda.church/api"
set UPLOAD_DIR "/opt/rtsda/church-api/uploads/events"
# Get token
set AUTH_RESPONSE (curl -s -X POST $API_BASE/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}')
set JWT_TOKEN (echo $AUTH_RESPONSE | jq -r '.data.token')
echo "✅ Got token"
# Get events for matching
set NEW_EVENTS (curl -s -H "Authorization: Bearer $JWT_TOKEN" "$API_BASE/events?perPage=500")
echo $NEW_EVENTS | jq '.data.items | map({id, title})' > new_events.json
set OLD_EVENTS (curl -s "$OLD_PB_BASE/collections/events/records?perPage=500")
echo $OLD_EVENTS | jq '.items | map({id, title})' > old_events.json
set uploaded 0
set failed 0
for event_dir in (find $STORAGE_PATH -mindepth 1 -maxdepth 1 -type d -name '[a-z0-9]*')
set old_id (basename $event_dir)
# Find ANY image file (jpeg, png, gif, webp)
set image_files (find $event_dir -maxdepth 1 -type f \( -name "*.webp" -o -name "*.jpeg" -o -name "*.jpg" -o -name "*.png" -o -name "*.gif" \) | grep -v "100x100_")
if test (count $image_files) -eq 0
continue
end
set image_file $image_files[1] # Take the first image
# Get old event and find new match
set old_event (cat old_events.json | jq --arg id "$old_id" '.[] | select(.id == $id)')
if test -z "$old_event"
continue
end
set title (echo $old_event | jq -r '.title')
set new_event (cat new_events.json | jq --arg title "$title" '.[] | select(.title == $title)')
if test -z "$new_event"
echo "❌ No match for: $title"
continue
end
set new_id (echo $new_event | jq -r '.id')
set original_filename (basename $image_file)
set extension (echo $original_filename | sed 's/.*\.//')
# Create WebP filename
set base_name (echo $original_filename | sed 's/\.[^.]*$//')
set webp_filename "$new_id-$base_name.webp"
set webp_path "$UPLOAD_DIR/$webp_filename"
echo "📤 Processing: $title"
echo " Source: $original_filename ($extension)"
echo " Target: $webp_filename"
# Convert to WebP (works for any input format)
if convert "$image_file" "$webp_path"
echo "✅ Converted to WebP"
# Update database
set current_event (curl -s -H "Authorization: Bearer $JWT_TOKEN" "$API_BASE/events/$new_id")
set simple_filename (echo $webp_filename | sed "s/^$new_id-//")
set image_path "uploads/events/$webp_filename"
set event_data (echo $current_event | jq --arg img "$simple_filename" --arg imgpath "$image_path" \
'.data | {
title: .title,
description: .description,
start_time: .start_time,
end_time: .end_time,
location: .location,
location_url: .location_url,
category: .category,
recurring_type: .recurring_type,
is_featured: .is_featured,
image: $img,
image_path: $imgpath
}')
set update_response (curl -s -X PUT \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "$event_data" \
"$API_BASE/admin/events/$new_id")
set success (echo $update_response | jq -r '.success // false')
if test "$success" = "true"
echo "✅ SUCCESS: $title"
set uploaded (math $uploaded + 1)
else
echo "❌ DB UPDATE FAILED: $title"
set failed (math $failed + 1)
end
else
echo "❌ CONVERSION FAILED: $title"
set failed (math $failed + 1)
end
echo "---"
sleep 0.1
end
rm -f new_events.json old_events.json
echo ""
echo "🎉 FINAL RESULTS!"
echo "================="
echo "✅ Successfully converted: $uploaded images"
echo "❌ Failed: $failed images"
echo ""
echo "All images now converted to WebP format!"

View file

@ -1,119 +0,0 @@
import json
import psycopg2
import os
from datetime import datetime
import uuid
# Connect to database
conn = psycopg2.connect(os.environ['DATABASE_URL'])
cur = conn.cursor()
def load_json(filename):
try:
with open(f'/tmp/pb_migration/{filename}', 'r') as f:
data = json.load(f)
return data.get('items', [])
except Exception as e:
print(f"Error loading {filename}: {e}")
return []
def convert_pb_date(pb_date):
"""Convert PocketBase date to PostgreSQL timestamp"""
if not pb_date:
return None
try:
# Remove 'Z' and parse
dt_str = pb_date.replace('Z', '+00:00')
return datetime.fromisoformat(dt_str)
except:
print(f"Failed to parse date: {pb_date}")
return None
# Clear existing data (except users)
print("🧹 Clearing existing data...")
cur.execute("DELETE FROM bulletins WHERE id != '00000000-0000-0000-0000-000000000000'")
cur.execute("DELETE FROM events WHERE id != '00000000-0000-0000-0000-000000000000'")
# Import bulletins
print("📄 Importing bulletins...")
bulletins = load_json('bulletins.json')
print(f"Found {len(bulletins)} bulletins to import")
for bulletin in bulletins:
try:
cur.execute("""
INSERT INTO bulletins (id, title, date, url, pdf_url, is_active, pdf_file,
sabbath_school, divine_worship, scripture_reading, sunset,
cover_image, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
str(uuid.uuid4()),
bulletin.get('title', ''),
bulletin.get('date'), # PocketBase dates should work directly
bulletin.get('url'),
bulletin.get('pdf_url'),
bulletin.get('is_active', True),
bulletin.get('pdf'),
bulletin.get('sabbath_school', ''),
bulletin.get('divine_worship', ''),
bulletin.get('scripture_reading'),
bulletin.get('sunset', ''),
bulletin.get('cover_image'),
convert_pb_date(bulletin.get('created')),
convert_pb_date(bulletin.get('updated'))
))
print(f" ✅ Imported: {bulletin.get('title')}")
except Exception as e:
print(f" ❌ Failed to import bulletin: {e}")
print(f" Data: {bulletin}")
# Import events
print("📅 Importing events...")
events = load_json('events.json')
print(f"Found {len(events)} events to import")
for event in events:
try:
cur.execute("""
INSERT INTO events (id, title, description, start_time, end_time, location,
location_url, image, thumbnail, category, is_featured,
recurring_type, approved_from, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
str(uuid.uuid4()),
event.get('title', ''),
event.get('description', ''),
event.get('start_time'), # Let PostgreSQL handle the date conversion
event.get('end_time'),
event.get('location', ''),
event.get('location_url'),
event.get('image'),
event.get('thumbnail'),
event.get('category', 'Other'),
event.get('is_featured', False),
event.get('reoccuring'), # Note: PocketBase spells it 'reoccuring'
event.get('approved_from'),
convert_pb_date(event.get('created')),
convert_pb_date(event.get('updated'))
))
print(f" ✅ Imported: {event.get('title')}")
except Exception as e:
print(f" ❌ Failed to import event: {e}")
print(f" Data: {event}")
# Commit changes
conn.commit()
print("✅ Migration fixed!")
# Show results
cur.execute("SELECT COUNT(*) FROM bulletins")
bulletin_count = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM events")
event_count = cur.fetchone()[0]
print(f"📊 Results:")
print(f" Bulletins: {bulletin_count}")
print(f" Events: {event_count}")
cur.close()
conn.close()

View file

@ -1,138 +0,0 @@
import json
import psycopg2
import os
from datetime import datetime
import uuid
# Connect to database
conn = psycopg2.connect(os.environ['DATABASE_URL'])
cur = conn.cursor()
def load_json(filename):
try:
with open(f'/tmp/pb_migration/{filename}', 'r') as f:
data = json.load(f)
return data.get('items', [])
except Exception as e:
print(f"Error loading {filename}: {e}")
return []
def convert_pb_date(pb_date):
"""Convert PocketBase date to PostgreSQL timestamp"""
if not pb_date:
return None
try:
dt_str = pb_date.replace('Z', '+00:00')
return datetime.fromisoformat(dt_str)
except:
return None
def clean_recurring_type(value):
"""Clean recurring type field"""
if not value or value == '':
return None
return value
# Rollback any pending transaction
conn.rollback()
# Clear existing data
print("🧹 Clearing existing data...")
cur.execute("DELETE FROM bulletins")
cur.execute("DELETE FROM events")
# Import bulletins
print("📄 Importing bulletins...")
bulletins = load_json('bulletins.json')
print(f"Found {len(bulletins)} bulletins to import")
for i, bulletin in enumerate(bulletins):
try:
cur.execute("""
INSERT INTO bulletins (id, title, date, url, pdf_url, is_active, pdf_file,
sabbath_school, divine_worship, scripture_reading, sunset,
cover_image, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
str(uuid.uuid4()),
bulletin.get('title', ''),
bulletin.get('date'),
bulletin.get('url'),
bulletin.get('pdf_url'),
bulletin.get('is_active', True),
bulletin.get('pdf'),
bulletin.get('sabbath_school', ''),
bulletin.get('divine_worship', ''),
bulletin.get('scripture_reading'),
bulletin.get('sunset', ''),
bulletin.get('cover_image'),
convert_pb_date(bulletin.get('created')),
convert_pb_date(bulletin.get('updated'))
))
print(f" ✅ Imported bulletin {i+1}: {bulletin.get('title')}")
except Exception as e:
print(f" ❌ Failed to import bulletin {i+1}: {e}")
continue
# Import events
print("📅 Importing events...")
events = load_json('events.json')
print(f"Found {len(events)} events to import")
for i, event in enumerate(events):
try:
cur.execute("""
INSERT INTO events (id, title, description, start_time, end_time, location,
location_url, image, thumbnail, category, is_featured,
recurring_type, approved_from, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
str(uuid.uuid4()),
event.get('title', ''),
event.get('description', ''),
event.get('start_time'),
event.get('end_time'),
event.get('location', ''),
event.get('location_url'),
event.get('image'),
event.get('thumbnail'),
event.get('category', 'Other'),
event.get('is_featured', False),
clean_recurring_type(event.get('reoccuring')), # Fix this field
event.get('approved_from') if event.get('approved_from') else None,
convert_pb_date(event.get('created')),
convert_pb_date(event.get('updated'))
))
print(f" ✅ Imported event {i+1}: {event.get('title')}")
except Exception as e:
print(f" ❌ Failed to import event {i+1}: {e}")
print(f" Title: {event.get('title')}")
continue
# Commit all changes
conn.commit()
print("✅ Migration completed!")
# Show final results
cur.execute("SELECT COUNT(*) FROM bulletins")
bulletin_count = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM events")
event_count = cur.fetchone()[0]
print(f"📊 Final Results:")
print(f" Bulletins: {bulletin_count}")
print(f" Events: {event_count}")
# Show sample data
print(f"\n📄 Sample bulletins:")
cur.execute("SELECT title, date FROM bulletins ORDER BY date DESC LIMIT 3")
for row in cur.fetchall():
print(f" - {row[0]} ({row[1]})")
print(f"\n📅 Sample events:")
cur.execute("SELECT title, start_time FROM events ORDER BY start_time LIMIT 3")
for row in cur.fetchall():
print(f" - {row[0]} ({row[1]})")
cur.close()
conn.close()

View file

@ -1,84 +0,0 @@
#!/usr/bin/env fish
echo "🔧 ACTUALLY FIXING THE ROUTES (NO BULLSHIT)"
echo "============================================"
# Backup first
cp src/main.rs src/main.rs.backup
# Fix admin routes: move pending routes before generic :id routes
sed -i '' '
/\.route("\/events\/:id", put(handlers::events::update))/i\
.route("/events/pending", get(handlers::events::list_pending))\
.route("/events/pending/:id/approve", post(handlers::events::approve))\
.route("/events/pending/:id/reject", post(handlers::events::reject))\
.route("/events/pending/:id", delete(handlers::events::delete_pending))
' src/main.rs
# Remove the old pending routes that are now duplicated
sed -i '' '/\.route("\/events\/pending", get(handlers::events::list_pending))/d' src/main.rs
sed -i '' '/\.route("\/events\/pending\/:id\/approve", post(handlers::events::approve))/d' src/main.rs
sed -i '' '/\.route("\/events\/pending\/:id\/reject", post(handlers::events::reject))/d' src/main.rs
sed -i '' '/\.route("\/events\/pending\/:id", delete(handlers::events::delete_pending))/d' src/main.rs
# Fix public routes: move submit before :id
sed -i '' '
/\.route("\/api\/events\/:id", get(handlers::events::get))/i\
.route("/api/events/submit", post(handlers::events::submit))
' src/main.rs
# Remove the old submit route
sed -i '' '/\.route("\/api\/events\/submit", post(handlers::events::submit))/d' src/main.rs
echo "✅ Routes reordered"
# Build and test
if cargo build
echo "✅ Build successful!"
# Restart server
sudo systemctl restart church-api
sleep 3
# Test it works
set AUTH_RESPONSE (curl -s -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}')
set JWT_TOKEN (echo $AUTH_RESPONSE | jq -r '.data.token')
echo "🧪 Testing pending events endpoint..."
set PENDING_TEST (curl -s -H "Authorization: Bearer $JWT_TOKEN" \
"https://api.rockvilletollandsda.church/api/admin/events/pending")
if echo $PENDING_TEST | grep -q success
echo "✅ PENDING EVENTS WORKING!"
else
echo "❌ Still broken: $PENDING_TEST"
end
echo "🧪 Testing submit endpoint..."
echo "test" > test.txt
set SUBMIT_TEST (curl -s -X POST https://api.rockvilletollandsda.church/api/events/submit \
-H "Authorization: Bearer $JWT_TOKEN" \
-F "title=Route Test" \
-F "description=Testing" \
-F "start_time=2025-07-01T18:00" \
-F "end_time=2025-07-01T19:00" \
-F "location=Test" \
-F "category=Other" \
-F "bulletin_week=current" \
-F "image=@test.txt")
if echo $SUBMIT_TEST | grep -q success
echo "✅ SUBMIT WORKING!"
echo "🎉 ALL ROUTES FIXED!"
else
echo "❌ Submit still broken: $SUBMIT_TEST"
end
rm -f test.txt
else
echo "❌ Build failed, restoring backup"
cp src/main.rs.backup src/main.rs
end

View file

@ -1,334 +0,0 @@
-- Fix Timezone Double Conversion
-- File: fix_timezone_double_conversion.sql
--
-- PROBLEM: The migration script converted EST times to UTC, but the original times
-- were already in EST (not UTC as assumed). This resulted in times being converted
-- backwards, making events appear 4-5 hours earlier than they should be.
--
-- SOLUTION: Restore original times from backup tables. These original times were
-- already in the correct EST format that the V1 API expects.
--
-- VALIDATION RESULTS SHOWING DOUBLE CONVERSION:
-- - Original: 2025-06-01 15:00:00 (3 PM EST - correct)
-- - Current: 2025-06-01 11:00:00 (11 AM UTC → 7 AM EDT display - wrong!)
-- - Offset: -4.0 hours (confirms backwards conversion)
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Start transaction for atomic restoration
BEGIN;
-- ================================
-- VALIDATION BEFORE RESTORATION
-- ================================
DO $$
DECLARE
backup_count INTEGER;
current_sample RECORD;
BEGIN
RAISE NOTICE '========================================';
RAISE NOTICE 'TIMEZONE DOUBLE CONVERSION FIX';
RAISE NOTICE 'Started at: %', NOW();
RAISE NOTICE '========================================';
-- Check backup tables exist
SELECT COUNT(*) INTO backup_count
FROM information_schema.tables
WHERE table_name LIKE '%timezone_backup';
RAISE NOTICE 'Found % backup tables', backup_count;
IF backup_count < 8 THEN
RAISE EXCEPTION 'Insufficient backup tables found (%). Cannot proceed without backups.', backup_count;
END IF;
-- Show current problematic times
RAISE NOTICE '';
RAISE NOTICE 'CURRENT PROBLEMATIC TIMES (Before Fix):';
FOR current_sample IN
SELECT
e.title,
e.start_time as current_utc,
e.start_time AT TIME ZONE 'America/New_York' as current_display,
eb.start_time as original_est
FROM events e
JOIN events_timezone_backup eb ON e.id = eb.id
WHERE e.start_time IS NOT NULL
ORDER BY e.start_time
LIMIT 3
LOOP
RAISE NOTICE 'Event: %', current_sample.title;
RAISE NOTICE ' Current UTC: %', current_sample.current_utc;
RAISE NOTICE ' Current Display: %', current_sample.current_display;
RAISE NOTICE ' Original EST: %', current_sample.original_est;
RAISE NOTICE '';
END LOOP;
END $$;
-- ================================
-- RESTORE ORIGINAL TIMES
-- ================================
RAISE NOTICE 'RESTORING ORIGINAL TIMES FROM BACKUPS...';
RAISE NOTICE '';
-- Restore events table
UPDATE events
SET
start_time = eb.start_time,
end_time = eb.end_time,
created_at = eb.created_at,
updated_at = eb.updated_at
FROM events_timezone_backup eb
WHERE events.id = eb.id;
-- Get count of restored events
DO $$
DECLARE
events_restored INTEGER;
BEGIN
SELECT COUNT(*) INTO events_restored
FROM events e
JOIN events_timezone_backup eb ON e.id = eb.id
WHERE e.start_time IS NOT NULL;
RAISE NOTICE 'Events restored: %', events_restored;
END $$;
-- Restore pending_events table
UPDATE pending_events
SET
start_time = peb.start_time,
end_time = peb.end_time,
submitted_at = peb.submitted_at,
created_at = peb.created_at,
updated_at = peb.updated_at
FROM pending_events_timezone_backup peb
WHERE pending_events.id = peb.id;
-- Get count of restored pending events
DO $$
DECLARE
pending_restored INTEGER;
BEGIN
SELECT COUNT(*) INTO pending_restored
FROM pending_events pe
JOIN pending_events_timezone_backup peb ON pe.id = peb.id
WHERE pe.start_time IS NOT NULL;
RAISE NOTICE 'Pending events restored: %', pending_restored;
END $$;
-- Restore bulletins table
UPDATE bulletins
SET
created_at = bb.created_at,
updated_at = bb.updated_at
FROM bulletins_timezone_backup bb
WHERE bulletins.id = bb.id;
-- Restore users table
UPDATE users
SET
created_at = ub.created_at,
updated_at = ub.updated_at
FROM users_timezone_backup ub
WHERE users.id = ub.id;
-- Restore church_config table
UPDATE church_config
SET
created_at = ccb.created_at,
updated_at = ccb.updated_at
FROM church_config_timezone_backup ccb
WHERE church_config.id = ccb.id;
-- Restore schedules table (if exists)
DO $$
BEGIN
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'schedules') THEN
UPDATE schedules
SET
created_at = sb.created_at,
updated_at = sb.updated_at
FROM schedules_timezone_backup sb
WHERE schedules.id = sb.id;
END IF;
END $$;
-- Restore bible_verses table (if exists)
DO $$
BEGIN
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'bible_verses') THEN
UPDATE bible_verses
SET
created_at = bvb.created_at,
updated_at = bvb.updated_at
FROM bible_verses_timezone_backup bvb
WHERE bible_verses.id = bvb.id;
END IF;
END $$;
-- Restore app_versions table (if exists)
DO $$
BEGIN
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'app_versions') THEN
UPDATE app_versions
SET
created_at = avb.created_at,
updated_at = avb.updated_at
FROM app_versions_timezone_backup avb
WHERE app_versions.id = avb.id;
END IF;
END $$;
-- ================================
-- POST-RESTORATION VALIDATION
-- ================================
DO $$
DECLARE
restored_sample RECORD;
total_events INTEGER;
total_pending INTEGER;
BEGIN
RAISE NOTICE '';
RAISE NOTICE 'POST-RESTORATION VALIDATION:';
RAISE NOTICE '';
-- Show restored times
FOR restored_sample IN
SELECT
title,
start_time as restored_est,
start_time AT TIME ZONE 'America/New_York' as display_time
FROM events
WHERE start_time IS NOT NULL
ORDER BY start_time
LIMIT 3
LOOP
RAISE NOTICE 'Event: %', restored_sample.title;
RAISE NOTICE ' Restored EST: %', restored_sample.restored_est;
RAISE NOTICE ' Display Time: %', restored_sample.display_time;
RAISE NOTICE '';
END LOOP;
-- Get totals
SELECT COUNT(*) INTO total_events FROM events WHERE start_time IS NOT NULL;
SELECT COUNT(*) INTO total_pending FROM pending_events WHERE start_time IS NOT NULL;
RAISE NOTICE 'RESTORATION SUMMARY:';
RAISE NOTICE '- Events with times: %', total_events;
RAISE NOTICE '- Pending with times: %', total_pending;
RAISE NOTICE '';
END $$;
-- ================================
-- UPDATE MIGRATION LOG
-- ================================
-- Record the fix in migration log
INSERT INTO migration_log (migration_name, description)
VALUES (
'fix_timezone_double_conversion',
'Fixed double timezone conversion by restoring original EST times from backup tables. The original migration incorrectly assumed UTC times when they were already in EST, causing events to display 4-5 hours earlier than intended.'
);
-- ================================
-- FINAL VALIDATION QUERIES
-- ================================
-- Create validation queries for manual verification
CREATE TEMP TABLE post_fix_validation AS
SELECT 1 as query_num,
'Verify event times now display correctly' as description,
$val1$
SELECT
title,
start_time as est_time,
start_time AT TIME ZONE 'America/New_York' as ny_display,
EXTRACT(hour FROM start_time) as hour_est
FROM events
WHERE start_time IS NOT NULL
ORDER BY start_time
LIMIT 10;
$val1$ as query_sql
UNION ALL
SELECT 2 as query_num,
'Check that event hours are reasonable (6 AM - 11 PM)' as description,
$val2$
SELECT
title,
start_time,
EXTRACT(hour FROM start_time) as event_hour,
CASE
WHEN EXTRACT(hour FROM start_time) BETWEEN 6 AND 23 THEN 'REASONABLE'
ELSE 'UNUSUAL'
END as time_assessment
FROM events
WHERE start_time IS NOT NULL
ORDER BY start_time;
$val2$ as query_sql
UNION ALL
SELECT 3 as query_num,
'Verify V1 API will return correct times' as description,
$val3$
-- This simulates what the V1 API timezone conversion will produce
SELECT
title,
start_time as stored_est,
start_time AT TIME ZONE 'America/New_York' as v1_display_equivalent
FROM events
WHERE start_time IS NOT NULL
ORDER BY start_time
LIMIT 5;
$val3$ as query_sql;
-- Display validation queries
DO $$
DECLARE
val_record RECORD;
BEGIN
RAISE NOTICE '========================================';
RAISE NOTICE 'VALIDATION QUERIES - RUN THESE TO VERIFY:';
RAISE NOTICE '========================================';
FOR val_record IN SELECT * FROM post_fix_validation ORDER BY query_num LOOP
RAISE NOTICE 'Query %: %', val_record.query_num, val_record.description;
RAISE NOTICE '%', val_record.query_sql;
RAISE NOTICE '----------------------------------------';
END LOOP;
END $$;
-- ================================
-- COMPLETION MESSAGE
-- ================================
DO $$
BEGIN
RAISE NOTICE '========================================';
RAISE NOTICE 'TIMEZONE DOUBLE CONVERSION FIX COMPLETED';
RAISE NOTICE 'Completed at: %', NOW();
RAISE NOTICE '========================================';
RAISE NOTICE 'WHAT WAS FIXED:';
RAISE NOTICE '- Restored original EST times from backup tables';
RAISE NOTICE '- Fixed events showing at midnight/early morning hours';
RAISE NOTICE '- V1 API will now return correct EST times to frontend';
RAISE NOTICE '- V2 API logic should be updated to handle EST times properly';
RAISE NOTICE '========================================';
RAISE NOTICE 'NEXT STEPS:';
RAISE NOTICE '1. Run the validation queries above';
RAISE NOTICE '2. Test the frontend clients to confirm times display correctly';
RAISE NOTICE '3. Update V2 API to properly convert EST to UTC if needed';
RAISE NOTICE '4. Consider keeping backup tables until fully verified';
RAISE NOTICE '========================================';
END $$;
-- Commit the transaction
COMMIT;

View file

@ -1,13 +0,0 @@
-- Force update the specific bulletin with clean content and new timestamp
UPDATE bulletins
SET
scripture_reading = 'For as many of you as have been baptized into Christ have put on Christ. Galatians 3:27 KJV',
updated_at = NOW()
WHERE id = '192730b5-c11c-4513-a37d-2a8b320136a4';
-- Verify the update
SELECT id, title,
scripture_reading,
updated_at
FROM bulletins
WHERE id = '192730b5-c11c-4513-a37d-2a8b320136a4';

View file

@ -1,97 +0,0 @@
#!/usr/bin/env fish
echo "🔧 POPULATING IMAGE_PATH FIELD"
echo "=============================="
set API_BASE "https://api.rockvilletollandsda.church/api"
set UPLOAD_DIR "/opt/rtsda/church-api/uploads/events"
# Get token
set AUTH_RESPONSE (curl -s -X POST $API_BASE/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}')
set JWT_TOKEN (echo $AUTH_RESPONSE | jq -r '.data.token')
echo "✅ Got token"
# Get all events
set EVENTS_RESPONSE (curl -s -H "Authorization: Bearer $JWT_TOKEN" "$API_BASE/events?perPage=500")
echo $EVENTS_RESPONSE | jq '.data.items' > events.json
set updated 0
set failed 0
echo "🔍 Updating events with proper image_path..."
for event in (cat events.json | jq -c '.[]')
set event_id (echo $event | jq -r '.id')
set title (echo $event | jq -r '.title')
set current_image (echo $event | jq -r '.image // empty')
set current_image_path (echo $event | jq -r '.image_path // empty')
if test -z "$current_image"
continue
end
# Look for the actual uploaded file
set actual_file (find "$UPLOAD_DIR" -name "$event_id-*" -type f | head -1)
if test -n "$actual_file"
set correct_path (echo $actual_file | sed "s|$UPLOAD_DIR/|uploads/events/|")
echo "📤 $title"
echo " Current image: $current_image"
echo " Setting image_path: $correct_path"
# Get current event data
set current_event (curl -s -H "Authorization: Bearer $JWT_TOKEN" "$API_BASE/events/$event_id")
# Update with both image and image_path
set event_data (echo $current_event | jq --arg img "$current_image" --arg imgpath "$correct_path" \
'.data | {
title: .title,
description: .description,
start_time: .start_time,
end_time: .end_time,
location: .location,
location_url: .location_url,
category: .category,
recurring_type: .recurring_type,
is_featured: .is_featured,
image: $img,
image_path: $imgpath
}')
set update_response (curl -s -X PUT \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "$event_data" \
"$API_BASE/admin/events/$event_id")
set success (echo $update_response | jq -r '.success // false')
if test "$success" = "true"
echo " ✅ SUCCESS"
set updated (math $updated + 1)
else
echo " ❌ FAILED"
set failed (math $failed + 1)
end
else
echo "$title - no uploaded file found"
set failed (math $failed + 1)
end
echo "---"
sleep 0.1
end
rm -f events.json
echo ""
echo "🎉 RESULTS!"
echo "==========="
echo "✅ Updated image_path: $updated events"
echo "❌ Failed: $failed events"
echo ""
echo "Now the admin dashboard should use the proper image_path field!"

View file

@ -1,50 +0,0 @@
#!/usr/bin/env fish
echo "🔧 Consolidating image fields..."
# Function to run SQL commands
function run_sql
sudo -u postgres psql -d church_db -c "$argv[1]"
end
# SAFETY: Create backups first
echo "🛡️ Creating backups..."
run_sql "CREATE TABLE pending_events_backup AS SELECT * FROM pending_events;"
run_sql "CREATE TABLE events_backup AS SELECT * FROM events;"
echo "📊 Checking current data before migration..."
run_sql "SELECT COUNT(*) as total_pending FROM pending_events;"
run_sql "SELECT COUNT(*) as total_events FROM events;"
echo "🔍 Showing sample data structure..."
run_sql "SELECT id, image, image_path FROM pending_events LIMIT 3;"
run_sql "SELECT id, image, image_path FROM events LIMIT 3;"
echo "📋 Records that will be affected by consolidation..."
run_sql "SELECT COUNT(*) as pending_needs_copy FROM pending_events WHERE image IS NULL AND image_path IS NOT NULL;"
run_sql "SELECT COUNT(*) as events_needs_copy FROM events WHERE image IS NULL AND image_path IS NOT NULL;"
echo "⚠️ SAFETY CHECK: Review the above data. Press ENTER to continue or Ctrl+C to abort..."
read
echo "📝 Copying image_path data to image column..."
run_sql "UPDATE pending_events SET image = image_path WHERE image IS NULL AND image_path IS NOT NULL;"
run_sql "UPDATE events SET image = image_path WHERE image IS NULL AND image_path IS NOT NULL;"
echo "✅ Verifying consolidation..."
run_sql "SELECT COUNT(*) as pending_with_image FROM pending_events WHERE image IS NOT NULL;"
run_sql "SELECT COUNT(*) as events_with_image FROM events WHERE image IS NOT NULL;"
echo "🔍 Sample data after consolidation..."
run_sql "SELECT id, image, image_path FROM pending_events LIMIT 3;"
echo "⚠️ Ready to drop image_path columns. Press ENTER to continue or Ctrl+C to abort..."
read
echo "🗑️ Dropping image_path columns..."
run_sql "ALTER TABLE pending_events DROP COLUMN image_path;"
run_sql "ALTER TABLE events DROP COLUMN image_path;"
echo "🎉 Migration complete!"
echo "📋 Backup tables created: pending_events_backup, events_backup"
echo "💡 To rollback: DROP the current tables and rename backups back"

View file

@ -1,314 +0,0 @@
#!/bin/bash
# PocketBase to PostgreSQL Migration Script
set -e
echo "🚀 Migrating PocketBase data to PostgreSQL..."
# Configuration
POCKETBASE_URL="http://localhost:8090" # Adjust if different
POSTGRES_URL="$DATABASE_URL"
MIGRATION_DIR="/tmp/pb_migration"
API_URL="https://api.rockvilletollandsda.church"
# Create migration directory
mkdir -p "$MIGRATION_DIR"
cd "$MIGRATION_DIR"
echo "📦 Step 1: Export data from PocketBase..."
# Function to export PocketBase collection data
export_collection() {
local collection=$1
echo " Exporting $collection..."
# Get all records from collection (adjust perPage if you have many records)
curl -s "${POCKETBASE_URL}/api/collections/${collection}/records?perPage=500" \
-o "${collection}.json"
if [ $? -eq 0 ]; then
echo " ✅ Exported $(jq '.items | length' ${collection}.json) records from $collection"
else
echo " ❌ Failed to export $collection"
fi
}
# Export all collections
export_collection "bulletins"
export_collection "events"
export_collection "pending_events"
export_collection "config"
export_collection "bible_verses"
export_collection "Quarterly_Schedule"
export_collection "Offering_and_Sunset_Times_Schedule"
export_collection "rtsda_android"
echo "📥 Step 2: Transform and import data..."
# Create Python script for data transformation
cat > transform_data.py << 'EOF'
import json
import sys
import uuid
from datetime import datetime
import psycopg2
from psycopg2.extras import RealDictCursor
import os
# Database connection
conn = psycopg2.connect(os.environ['DATABASE_URL'])
cur = conn.cursor(cursor_factory=RealDictCursor)
def load_json(filename):
try:
with open(filename, 'r') as f:
data = json.load(f)
return data.get('items', [])
except FileNotFoundError:
print(f"⚠️ {filename} not found, skipping...")
return []
def convert_date(pb_date):
"""Convert PocketBase date to PostgreSQL format"""
if not pb_date:
return None
try:
# PocketBase uses ISO format
dt = datetime.fromisoformat(pb_date.replace('Z', '+00:00'))
return dt
except:
return None
def generate_uuid():
"""Generate PostgreSQL-compatible UUID"""
return str(uuid.uuid4())
print("🔄 Transforming bulletins...")
bulletins = load_json('bulletins.json')
for bulletin in bulletins:
cur.execute("""
INSERT INTO bulletins (id, title, date, url, pdf_url, is_active, pdf_file,
sabbath_school, divine_worship, scripture_reading, sunset,
cover_image, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
bulletin.get('title'),
convert_date(bulletin.get('date')),
bulletin.get('url'),
bulletin.get('pdf_url'),
bulletin.get('is_active', True),
bulletin.get('pdf'),
bulletin.get('sabbath_school'),
bulletin.get('divine_worship'),
bulletin.get('scripture_reading'),
bulletin.get('sunset'),
bulletin.get('cover_image'),
convert_date(bulletin.get('created')),
convert_date(bulletin.get('updated'))
))
print("🔄 Transforming events...")
events = load_json('events.json')
for event in events:
cur.execute("""
INSERT INTO events (id, title, description, start_time, end_time, location,
location_url, image, thumbnail, category, is_featured,
recurring_type, approved_from, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
event.get('title'),
event.get('description'),
convert_date(event.get('start_time')),
convert_date(event.get('end_time')),
event.get('location'),
event.get('location_url'),
event.get('image'),
event.get('thumbnail'),
event.get('category'),
event.get('is_featured', False),
event.get('reoccuring'), # Note: PB uses 'reoccuring', PG uses 'recurring_type'
event.get('approved_from'),
convert_date(event.get('created')),
convert_date(event.get('updated'))
))
print("🔄 Transforming pending events...")
pending_events = load_json('pending_events.json')
for event in pending_events:
cur.execute("""
INSERT INTO pending_events (id, title, description, start_time, end_time, location,
location_url, image, thumbnail, category, is_featured,
recurring_type, approval_status, submitted_at, bulletin_week,
admin_notes, submitter_email, email_sent, pending_email_sent,
rejection_email_sent, approval_email_sent, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
event.get('title'),
event.get('description'),
convert_date(event.get('start_time')),
convert_date(event.get('end_time')),
event.get('location'),
event.get('location_url'),
event.get('image'),
event.get('thumbnail'),
event.get('category'),
event.get('is_featured', False),
event.get('reoccuring'),
event.get('approval_status', 'pending'),
convert_date(event.get('submitted_at')),
event.get('bulletin_week'),
event.get('admin_notes'),
event.get('submitter_email'),
event.get('email_sent', False),
event.get('pending_email_sent', False),
event.get('rejection_email_sent', False),
event.get('approval_email_sent', False),
convert_date(event.get('created')),
convert_date(event.get('updated'))
))
print("🔄 Transforming church config...")
configs = load_json('config.json')
for config in configs:
cur.execute("""
INSERT INTO church_config (id, church_name, contact_email, contact_phone,
church_address, po_box, google_maps_url, about_text,
api_keys, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
config.get('church_name'),
config.get('contact_email'),
config.get('contact_phone'),
config.get('church_address'),
config.get('po_box'),
config.get('google_maps_url'),
config.get('about_text'),
json.dumps(config.get('api_key', {})),
convert_date(config.get('created')),
convert_date(config.get('updated'))
))
print("🔄 Transforming bible verses...")
verses = load_json('bible_verses.json')
for verse_record in verses:
cur.execute("""
INSERT INTO bible_verses (id, verses, created_at, updated_at)
VALUES (%s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
json.dumps(verse_record.get('verses', {})),
convert_date(verse_record.get('created')),
convert_date(verse_record.get('updated'))
))
print("🔄 Transforming schedules...")
# Quarterly schedules
quarterly = load_json('Quarterly_Schedule.json')
for schedule in quarterly:
cur.execute("""
INSERT INTO schedules (id, schedule_type, year, quarter, schedule_data, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
'quarterly',
schedule.get('year'),
schedule.get('quarter'),
json.dumps(schedule.get('schedule_data', {})),
convert_date(schedule.get('created')),
convert_date(schedule.get('updated'))
))
# Offering and sunset schedules
offering = load_json('Offering_and_Sunset_Times_Schedule.json')
for schedule in offering:
cur.execute("""
INSERT INTO schedules (id, schedule_type, year, quarter, schedule_data, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
'offering_sunset',
schedule.get('year'),
None,
json.dumps(schedule.get('schedule_data', {})),
convert_date(schedule.get('created')),
convert_date(schedule.get('updated'))
))
print("🔄 Transforming app versions...")
app_versions = load_json('rtsda_android.json')
for app in app_versions:
cur.execute("""
INSERT INTO app_versions (id, platform, version_name, version_code, download_url,
update_required, description, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO NOTHING
""", (
generate_uuid(),
'android',
app.get('version_name'),
app.get('version_code'),
None, # You'll need to set download URLs manually
app.get('update_required', False),
app.get('update_description'),
convert_date(app.get('created')),
convert_date(app.get('updated'))
))
# Commit all changes
conn.commit()
cur.close()
conn.close()
print("✅ Data transformation complete!")
EOF
# Install required Python packages
pip3 install psycopg2-binary > /dev/null 2>&1
# Run the transformation
python3 transform_data.py
echo "📊 Step 3: Verifying migration..."
# Check what was migrated
psql "$POSTGRES_URL" -c "
SELECT
'bulletins' as table_name, COUNT(*) as records FROM bulletins
UNION ALL SELECT
'events', COUNT(*) FROM events
UNION ALL SELECT
'pending_events', COUNT(*) FROM pending_events
UNION ALL SELECT
'church_config', COUNT(*) FROM church_config
UNION ALL SELECT
'bible_verses', COUNT(*) FROM bible_verses
UNION ALL SELECT
'schedules', COUNT(*) FROM schedules
UNION ALL SELECT
'app_versions', COUNT(*) FROM app_versions;
"
echo "🎉 Migration complete!"
echo ""
echo "📋 Next steps:"
echo "1. Verify data looks correct in PostgreSQL"
echo "2. Test API endpoints to ensure data is accessible"
echo "3. Update any file URLs that point to PocketBase"
echo "4. Shut down PocketBase once everything is working"
echo ""
echo "🧪 Test your migrated data:"
echo " curl $API_URL/api/bulletins"
echo " curl $API_URL/api/events"
# Cleanup
rm -rf "$MIGRATION_DIR"

View file

@ -1,93 +0,0 @@
#!/usr/bin/env fish
# Script to remove all image_path references from Rust code
echo "🧹 Cleaning up image_path references..."
# Backup original files first
echo "📦 Creating backups..."
set backup_dir "backup_before_image_path_removal_$(date +%Y%m%d_%H%M%S)"
mkdir -p $backup_dir
for file in src/models.rs src/db/events.rs src/handlers/events.rs src/upload.rs
if test -f $file
cp $file $backup_dir/
echo " ✓ Backed up $file"
end
end
echo ""
echo "🔧 Removing image_path references..."
# Function to safely remove lines containing image_path
function remove_image_path_lines
set file $argv[1]
if test -f $file
echo " Processing $file..."
# Remove lines that contain image_path (struct fields, variables, etc.)
sed -i '/image_path/d' $file
# Also remove any trailing commas that might be left hanging
sed -i '/^[[:space:]]*,$/d' $file
echo " ✓ Removed image_path references from $file"
else
echo " ⚠️ File $file not found"
end
end
# Process each file
remove_image_path_lines "src/models.rs"
remove_image_path_lines "src/handlers/events.rs"
remove_image_path_lines "src/upload.rs"
# For events.rs, we need more careful handling of SQL queries
echo " Processing src/db/events.rs (SQL queries)..."
if test -f "src/db/events.rs"
# Remove image_path from SQL UPDATE/INSERT statements and adjust parameter numbers
sed -i 's/, image_path = \$[0-9][0-9]*//g' src/db/events.rs
sed -i 's/image_path = \$[0-9][0-9]*,//g' src/db/events.rs
sed -i 's/image_path = \$[0-9][0-9]*//g' src/db/events.rs
sed -i 's/, image_path//g' src/db/events.rs
sed -i 's/image_path,//g' src/db/events.rs
sed -i '/image_path/d' src/db/events.rs
echo " ✓ Cleaned SQL queries in src/db/events.rs"
else
echo " ⚠️ File src/db/events.rs not found"
end
echo ""
echo "🔍 Checking for remaining references..."
set remaining (grep -r "image_path" src/ 2>/dev/null | wc -l)
if test $remaining -eq 0
echo "✅ All image_path references removed successfully!"
else
echo "⚠️ Found $remaining remaining references:"
grep -r "image_path" src/ --color=always
echo ""
echo "You may need to manually review these remaining references."
end
echo ""
echo "🧪 Running cargo check..."
if cargo check
echo "✅ Code compiles successfully!"
else
echo "❌ Compilation errors found. You may need to:"
echo " - Fix parameter indices in SQL queries"
echo " - Remove trailing commas"
echo " - Update function signatures"
echo ""
echo "💾 Your original files are backed up in: $backup_dir"
end
echo ""
echo "🎉 Cleanup complete!"
echo "💾 Backups saved in: $backup_dir"
echo "🔧 Next steps:"
echo " 1. Review any remaining compilation errors"
echo " 2. Test your application"
echo " 3. Remove backup directory when satisfied"

View file

@ -1,349 +0,0 @@
#!/bin/bash
# Complete Church API Implementation - ALL FILES AT ONCE!
set -e
echo "🦀 Deploying complete Church API functionality..."
cd /opt/rtsda/church-api
# Complete the pending events database functions that were cut off
cat >> src/db/events.rs << 'EOF'
req.title,
req.description,
req.start_time,
req.end_time,
req.location,
req.location_url,
req.category,
req.is_featured.unwrap_or(false),
req.recurring_type,
req.bulletin_week,
req.submitter_email
)
.fetch_one(pool)
.await?;
Ok(pending_event)
}
pub async fn list_pending(pool: &PgPool, page: i32, per_page: i32) -> Result<(Vec<PendingEvent>, i64)> {
let offset = (page - 1) * per_page;
let events = sqlx::query_as!(
PendingEvent,
"SELECT * FROM pending_events WHERE approval_status = 'pending' ORDER BY submitted_at DESC LIMIT $1 OFFSET $2",
per_page,
offset
)
.fetch_all(pool)
.await?;
let total = sqlx::query_scalar!("SELECT COUNT(*) FROM pending_events WHERE approval_status = 'pending'")
.fetch_one(pool)
.await?
.unwrap_or(0);
Ok((events, total))
}
pub async fn get_pending_by_id(pool: &PgPool, id: &Uuid) -> Result<Option<PendingEvent>> {
let event = sqlx::query_as!(PendingEvent, "SELECT * FROM pending_events WHERE id = $1", id)
.fetch_optional(pool)
.await?;
Ok(event)
}
pub async fn approve_pending(pool: &PgPool, id: &Uuid, admin_notes: Option<String>) -> Result<Event> {
// Start transaction to move from pending to approved
let mut tx = pool.begin().await?;
// Get the pending event
let pending = sqlx::query_as!(
PendingEvent,
"SELECT * FROM pending_events WHERE id = $1",
id
)
.fetch_one(&mut *tx)
.await?;
// Create the approved event
let event = sqlx::query_as!(
Event,
"INSERT INTO events (title, description, start_time, end_time, location, location_url, category, is_featured, recurring_type, approved_from)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *",
pending.title,
pending.description,
pending.start_time,
pending.end_time,
pending.location,
pending.location_url,
pending.category,
pending.is_featured,
pending.recurring_type,
pending.submitter_email
)
.fetch_one(&mut *tx)
.await?;
// Update pending event status
sqlx::query!(
"UPDATE pending_events SET approval_status = 'approved', admin_notes = $1, updated_at = NOW() WHERE id = $2",
admin_notes,
id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(event)
}
pub async fn reject_pending(pool: &PgPool, id: &Uuid, admin_notes: Option<String>) -> Result<()> {
let result = sqlx::query!(
"UPDATE pending_events SET approval_status = 'rejected', admin_notes = $1, updated_at = NOW() WHERE id = $2",
admin_notes,
id
)
.execute(pool)
.await?;
if result.rows_affected() == 0 {
return Err(ApiError::NotFound("Pending event not found".to_string()));
}
Ok(())
}
EOF
# Add config database module
cat > src/db/config.rs << 'EOF'
use sqlx::PgPool;
use uuid::Uuid;
use crate::{error::Result, models::ChurchConfig};
pub async fn get_config(pool: &PgPool) -> Result<Option<ChurchConfig>> {
let config = sqlx::query_as!(ChurchConfig, "SELECT * FROM church_config LIMIT 1")
.fetch_optional(pool)
.await?;
Ok(config)
}
pub async fn update_config(pool: &PgPool, config: ChurchConfig) -> Result<ChurchConfig> {
let updated = sqlx::query_as!(
ChurchConfig,
"UPDATE church_config SET
church_name = $1, contact_email = $2, contact_phone = $3,
church_address = $4, po_box = $5, google_maps_url = $6,
about_text = $7, api_keys = $8, updated_at = NOW()
WHERE id = $9
RETURNING *",
config.church_name,
config.contact_email,
config.contact_phone,
config.church_address,
config.po_box,
config.google_maps_url,
config.about_text,
config.api_keys,
config.id
)
.fetch_one(pool)
.await?;
Ok(updated)
}
EOF
# Update main.rs to include email support
cat > src/main.rs << 'EOF'
use anyhow::{Context, Result};
use axum::{
middleware,
routing::{delete, get, post, put},
Router,
};
use std::{env, sync::Arc};
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod auth;
mod db;
mod email;
mod error;
mod handlers;
mod models;
use email::{EmailConfig, Mailer};
#[derive(Clone)]
pub struct AppState {
pub pool: sqlx::PgPool,
pub jwt_secret: String,
pub mailer: Arc<Mailer>,
}
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "church_api=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
// Load environment variables
dotenvy::dotenv().ok();
let database_url = env::var("DATABASE_URL").context("DATABASE_URL must be set")?;
let jwt_secret = env::var("JWT_SECRET").context("JWT_SECRET must be set")?;
// Initialize database
let pool = sqlx::PgPool::connect(&database_url)
.await
.context("Failed to connect to database")?;
// Run migrations
sqlx::migrate!("./migrations")
.run(&pool)
.await
.context("Failed to run migrations")?;
// Initialize email
let email_config = EmailConfig::from_env().context("Failed to load email config")?;
let mailer = Arc::new(Mailer::new(email_config).context("Failed to initialize mailer")?);
let state = AppState {
pool,
jwt_secret,
mailer,
};
// Build our application with routes
let app = Router::new()
// Public routes (no auth required)
.route("/api/auth/login", post(handlers::auth::login))
.route("/api/bulletins", get(handlers::bulletins::list))
.route("/api/bulletins/current", get(handlers::bulletins::current))
.route("/api/bulletins/:id", get(handlers::bulletins::get))
.route("/api/events", get(handlers::events::list))
.route("/api/events/upcoming", get(handlers::events::upcoming))
.route("/api/events/featured", get(handlers::events::featured))
.route("/api/events/:id", get(handlers::events::get))
.route("/api/events/submit", post(handlers::events::submit))
// Protected admin routes
.route("/api/admin/users", get(handlers::auth::list_users))
.route("/api/admin/bulletins", post(handlers::bulletins::create))
.route("/api/admin/bulletins/:id", put(handlers::bulletins::update))
.route("/api/admin/bulletins/:id", delete(handlers::bulletins::delete))
.route("/api/admin/events", post(handlers::events::create))
.route("/api/admin/events/:id", put(handlers::events::update))
.route("/api/admin/events/:id", delete(handlers::events::delete))
.route("/api/admin/events/pending", get(handlers::events::list_pending))
.route("/api/admin/events/pending/:id/approve", post(handlers::events::approve))
.route("/api/admin/events/pending/:id/reject", post(handlers::events::reject))
.layer(middleware::from_fn_with_state(state.clone(), auth::auth_middleware))
.with_state(state)
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
tracing::info!("🚀 Church API server running on {}", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}
EOF
# Update Cargo.toml with all dependencies
cat > Cargo.toml << 'EOF'
[package]
name = "church-api"
version = "0.1.0"
edition = "2021"
[dependencies]
# Web framework
axum = { version = "0.7", features = ["multipart"] }
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
# Database
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Authentication & Security
jsonwebtoken = "9.2"
bcrypt = "0.15"
# Email
lettre = { version = "0.11", features = ["tokio1-rustls-tls", "smtp-transport", "builder"] }
# Utilities
uuid = { version = "1.6", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
dotenvy = "0.15"
rust_decimal = { version = "1.33", features = ["serde"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
EOF
# Update .env with email configuration
cat >> .env << 'EOF'
# Email Configuration (Fastmail SMTP)
SMTP_HOST=smtp.fastmail.com
SMTP_PORT=587
SMTP_USER=your-email@your-domain.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@rockvilletollandsda.church
ADMIN_EMAIL=admin@rockvilletollandsda.church
EOF
# Apply database migrations and restart services
echo "🗄️ Running database migrations..."
cargo sqlx migrate run
echo "🔄 Rebuilding and restarting services..."
cargo build --release
# Restart with systemd
sudo systemctl restart church-api
sudo systemctl restart nginx
echo "✅ COMPLETE! Your Church API now has:"
echo " • Real database operations with PostgreSQL"
echo " • Working email notifications via Fastmail SMTP"
echo " • JWT authentication system"
echo " • Event submission & approval workflow with emails"
echo " • File upload support ready"
echo " • Production-ready error handling"
echo ""
echo "🔧 Don't forget to update your .env file with real SMTP credentials!"
echo "📧 Test the email system by submitting an event at /api/events/submit"
echo "🚀 API Documentation at: http://your-domain.com/api/docs"

Binary file not shown.

View file

@ -1,177 +0,0 @@
#!/bin/bash
# Script to run HTML entity cleaning migration
# This script provides safety checks and backup functionality
set -e # Exit on any error
# Configuration
DB_URL="${DATABASE_URL:-postgresql://localhost/church_api}"
MIGRATION_FILE="migrations/20250811000001_clean_html_entities.sql"
TEST_FILE="test_html_cleaning_migration.sql"
BACKUP_DIR="./migration_backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}Church API - HTML Entity Cleaning Migration${NC}"
echo "============================================="
echo
# Check if migration file exists
if [ ! -f "$MIGRATION_FILE" ]; then
echo -e "${RED}Error: Migration file not found: $MIGRATION_FILE${NC}"
exit 1
fi
# Check if test file exists
if [ ! -f "$TEST_FILE" ]; then
echo -e "${RED}Error: Test file not found: $TEST_FILE${NC}"
exit 1
fi
# Create backup directory
mkdir -p "$BACKUP_DIR"
echo -e "${YELLOW}Step 1: Testing the cleaning function...${NC}"
echo "Running test script to verify the cleaning logic works correctly..."
# Run the test script
if psql "$DB_URL" -f "$TEST_FILE" > /dev/null 2>&1; then
echo -e "${GREEN}✓ Test passed! The cleaning function works correctly.${NC}"
else
echo -e "${RED}✗ Test failed! Please check the test output.${NC}"
echo "Test output:"
psql "$DB_URL" -f "$TEST_FILE"
exit 1
fi
echo
echo -e "${YELLOW}Step 2: Creating database backup...${NC}"
BACKUP_FILE="$BACKUP_DIR/backup_before_html_cleaning_$TIMESTAMP.sql"
# Create backup of affected tables
echo "Creating backup of tables that will be modified..."
pg_dump "$DB_URL" \
--table=bulletins \
--table=events \
--table=pending_events \
--table=members \
--table=church_config \
--table=media_items \
--table=transcoded_media \
--table=users \
--data-only \
--no-owner \
--no-privileges > "$BACKUP_FILE" 2>/dev/null || {
echo -e "${YELLOW}Note: Some tables may not exist, continuing with available tables...${NC}"
# Try with just the core tables that definitely exist
pg_dump "$DB_URL" \
--table=bulletins \
--table=events \
--table=pending_events \
--table=church_config \
--table=users \
--data-only \
--no-owner \
--no-privileges > "$BACKUP_FILE" 2>/dev/null || {
echo -e "${RED}Failed to create backup. Aborting migration.${NC}"
exit 1
}
}
echo -e "${GREEN}✓ Backup created: $BACKUP_FILE${NC}"
echo
echo -e "${YELLOW}Step 3: Analyzing current data for HTML entities...${NC}"
# Check for HTML entities in the database
ENTITY_COUNT=$(psql "$DB_URL" -t -c "
SELECT COUNT(*) FROM (
SELECT id FROM bulletins WHERE
title ~ '<[^>]*>' OR sabbath_school ~ '<[^>]*>' OR divine_worship ~ '<[^>]*>' OR
scripture_reading ~ '<[^>]*>' OR sunset ~ '<[^>]*>' OR
title ~ '&(nbsp|amp|lt|gt|quot|#39);' OR sabbath_school ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
divine_worship ~ '&(nbsp|amp|lt|gt|quot|#39);' OR scripture_reading ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
sunset ~ '&(nbsp|amp|lt|gt|quot|#39);'
UNION ALL
SELECT id FROM events WHERE
title ~ '<[^>]*>' OR description ~ '<[^>]*>' OR location ~ '<[^>]*>' OR
location_url ~ '<[^>]*>' OR approved_from ~ '<[^>]*>' OR
title ~ '&(nbsp|amp|lt|gt|quot|#39);' OR description ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
location ~ '&(nbsp|amp|lt|gt|quot|#39);' OR location_url ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
approved_from ~ '&(nbsp|amp|lt|gt|quot|#39);'
UNION ALL
SELECT id FROM pending_events WHERE
title ~ '<[^>]*>' OR description ~ '<[^>]*>' OR location ~ '<[^>]*>' OR
location_url ~ '<[^>]*>' OR admin_notes ~ '<[^>]*>' OR
title ~ '&(nbsp|amp|lt|gt|quot|#39);' OR description ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
location ~ '&(nbsp|amp|lt|gt|quot|#39);' OR admin_notes ~ '&(nbsp|amp|lt|gt|quot|#39);'
) AS dirty_records;
" | xargs)
echo "Found $ENTITY_COUNT records with HTML tags or entities that need cleaning."
if [ "$ENTITY_COUNT" -eq 0 ]; then
echo -e "${GREEN}✓ No HTML entities found! Database is already clean.${NC}"
echo "Migration can still be run to install the cleaning function for future use."
fi
echo
echo -e "${YELLOW}Step 4: Ready to run migration${NC}"
echo "This will:"
echo " • Install the clean_html_entities() function"
echo " • Clean HTML tags and entities from all text fields"
echo " • Update the updated_at timestamps for modified records"
echo " • Provide detailed logging of what was cleaned"
echo
echo "Backup location: $BACKUP_FILE"
# Ask for confirmation
read -p "Do you want to proceed with the migration? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Migration cancelled by user.${NC}"
exit 0
fi
echo
echo -e "${YELLOW}Step 5: Running migration...${NC}"
# Run the migration
if psql "$DB_URL" -f "$MIGRATION_FILE"; then
echo
echo -e "${GREEN}✓ Migration completed successfully!${NC}"
echo
echo -e "${BLUE}Summary:${NC}"
echo "• HTML tags and entities have been cleaned from all text fields"
echo "• Database backup is available at: $BACKUP_FILE"
echo "• The clean_html_entities() function is now available for future use"
echo "• All API responses will now return clean data"
echo
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Test your API endpoints to verify clean data"
echo "2. Monitor for any issues with data formatting"
echo "3. Keep the backup file until you're confident everything works correctly"
echo
echo -e "${GREEN}Migration completed successfully!${NC}"
else
echo -e "${RED}✗ Migration failed!${NC}"
echo
echo -e "${YELLOW}Rollback instructions:${NC}"
echo "1. Restore from backup: psql \"$DB_URL\" < \"$BACKUP_FILE\""
echo "2. Check the migration logs above for error details"
echo "3. Fix any issues and try again"
exit 1
fi

View file

@ -1,50 +0,0 @@
#!/bin/bash
echo "🔍 SERVER-SIDE DEBUG (Run this on the actual server)"
echo "=================================================="
# Check if we're on the server
if [ ! -f "/opt/rtsda/church-api/Cargo.toml" ]; then
echo "❌ This script must be run on the server (rockvilleavdesktop)"
echo " SSH to the server and run this script there"
exit 1
fi
echo "✅ Running on server"
# Check uploads directory
echo "📁 Checking uploads directory..."
if [ -d "/opt/rtsda/church-api/uploads/events" ]; then
echo "✅ uploads/events exists"
echo "Files:"
ls -la /opt/rtsda/church-api/uploads/events/
else
echo "❌ uploads/events directory not found"
echo "Creating it..."
mkdir -p /opt/rtsda/church-api/uploads/events
chown rockvilleav:rockvilleav /opt/rtsda/church-api/uploads/events
echo "✅ Created uploads/events directory"
fi
# Check server logs
echo ""
echo "📜 Recent server logs..."
journalctl -u church-api --since "5 minutes ago" --no-pager | tail -20
# Check the pending events endpoint issue
echo ""
echo "🔍 Testing pending events endpoint..."
AUTH_TOKEN=$(curl -s -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}' \
| jq -r '.data.token')
echo "Testing: https://api.rockvilletollandsda.church/api/events/pending"
curl -v -H "Authorization: Bearer $AUTH_TOKEN" \
"https://api.rockvilletollandsda.church/api/events/pending" 2>&1
echo ""
echo "🎯 What to check:"
echo "1. Are WebP files being created in uploads/events/?"
echo "2. What's the UUID parsing error in pending events?"
echo "3. Are there any crash logs in journalctl?"

View file

@ -1,379 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎯 Smart Video Streaming Test</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #111;
color: #fff;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 { color: #e50914; }
.video-container {
background: #222;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
video {
width: 100%;
max-width: 800px;
height: auto;
border-radius: 5px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.info-card {
background: #333;
padding: 15px;
border-radius: 8px;
border: 2px solid transparent;
}
.info-card.active { border-color: #e50914; }
.info-card h3 {
margin: 0 0 10px 0;
color: #e50914;
}
.info-card p { margin: 5px 0; }
.status {
background: #444;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
font-family: monospace;
}
.controls {
text-align: center;
margin: 20px 0;
}
button {
background: #e50914;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
font-size: 16px;
}
button:hover { background: #f40612; }
.codec-support {
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
.codec-support h3 { color: #4CAF50; }
.codec {
display: inline-block;
padding: 5px 10px;
margin: 5px;
border-radius: 3px;
font-family: monospace;
}
.codec.supported { background: #27ae60; color: white; }
.codec.unsupported { background: #e74c3c; color: white; }
.log {
background: #000;
color: #0f0;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
margin: 20px 0;
}
.streaming-method {
font-weight: bold;
padding: 5px 10px;
border-radius: 3px;
display: inline-block;
margin: 5px 0;
}
.direct { background: #27ae60; color: white; }
.hls { background: #f39c12; color: white; }
.transcoding { background: #e74c3c; color: white; }
</style>
</head>
<body>
<div class="container">
<h1>🎯 Smart Video Streaming Test</h1>
<p>Like Jellyfin but not garbage C# - serves AV1 directly to modern browsers, HLS to legacy clients</p>
<div class="codec-support">
<h3>Your Browser Codec Support</h3>
<div id="codecSupport"></div>
</div>
<div class="video-container">
<video id="videoPlayer" controls>
Your browser does not support the video tag.
</video>
<div id="videoStatus" class="status">Click "Start Smart Stream" to begin</div>
</div>
<div class="controls">
<button onclick="startSmartStream()">🚀 Start Smart Stream</button>
<button onclick="testHLS()">📺 Test HLS Fallback</button>
<button onclick="clearLog()">🧹 Clear Log</button>
</div>
<div class="info-grid">
<div class="info-card" id="streamingCard">
<h3>🎬 Streaming Method</h3>
<p>Method: <span id="streamingMethod">Unknown</span></p>
<p>Codec: <span id="streamingCodec">Unknown</span></p>
<p>Source: <span id="streamingSource">Unknown</span></p>
</div>
<div class="info-card" id="performanceCard">
<h3>⚡ Performance</h3>
<p>Load Time: <span id="loadTime">-</span></p>
<p>File Size: <span id="fileSize">-</span></p>
<p>Bitrate: <span id="bitrate">-</span></p>
</div>
<div class="info-card" id="networkCard">
<h3>🌐 Network</h3>
<p>Response: <span id="responseCode">-</span></p>
<p>Headers: <span id="responseHeaders">-</span></p>
<p>Cached: <span id="cached">-</span></p>
</div>
</div>
<div class="log" id="log"></div>
</div>
<script>
const API_BASE = window.location.origin;
const MEDIA_ID = '123e4567-e89b-12d3-a456-426614174000';
const video = document.getElementById('videoPlayer');
// Detect codec support
function detectCodecSupport() {
const codecs = [
{ name: 'AV1', mime: 'video/mp4; codecs="av01.0.08M.08"' },
{ name: 'HEVC/H.265', mime: 'video/mp4; codecs="hvc1.1.6.L93.B0"' },
{ name: 'H.264', mime: 'video/mp4; codecs="avc1.42E01E"' },
{ name: 'VP9', mime: 'video/webm; codecs="vp9"' },
{ name: 'VP8', mime: 'video/webm; codecs="vp8"' }
];
// Try multiple HEVC variants for Safari compatibility
const hevcVariants = [
'video/mp4; codecs="hvc1.1.6.L93.B0"',
'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/mp4; codecs="hvc1.1.60.L93.B0"',
'video/mp4; codecs="hvc1"'
];
let hevcSupported = false;
for (const variant of hevcVariants) {
if (video.canPlayType(variant) !== '') {
hevcSupported = true;
break;
}
}
const supportDiv = document.getElementById('codecSupport');
supportDiv.innerHTML = codecs.map(codec => {
let supported;
if (codec.name === 'HEVC/H.265') {
supported = hevcSupported; // Use our enhanced HEVC detection
} else {
supported = video.canPlayType(codec.mime) !== '';
}
return `<span class="codec ${supported ? 'supported' : 'unsupported'}">${codec.name}</span>`;
}).join('');
// Log user agent for debugging
log(`🔍 User Agent: ${navigator.userAgent}`, 'info');
return {
av1: video.canPlayType('video/mp4; codecs="av01.0.08M.08"') !== '',
hevc: hevcSupported, // Use enhanced HEVC detection
h264: video.canPlayType('video/mp4; codecs="avc1.42E01E"') !== ''
};
}
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
const className = type;
logDiv.innerHTML += `<div>[${timestamp}] ${message}</div>`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
function updateStatus(message) {
document.getElementById('videoStatus').textContent = message;
}
function updateStreamingInfo(method, codec, source) {
document.getElementById('streamingMethod').innerHTML =
`<span class="streaming-method ${method.toLowerCase()}">${method}</span>`;
document.getElementById('streamingCodec').textContent = codec;
document.getElementById('streamingSource').textContent = source;
document.getElementById('streamingCard').classList.add('active');
}
function updatePerformanceInfo(loadTime, fileSize, bitrate) {
document.getElementById('loadTime').textContent = `${loadTime}ms`;
document.getElementById('fileSize').textContent = formatBytes(fileSize);
document.getElementById('bitrate').textContent = bitrate ? `${(bitrate / 1000).toFixed(1)} Mbps` : 'Unknown';
document.getElementById('performanceCard').classList.add('active');
}
function updateNetworkInfo(status, headers, cached) {
document.getElementById('responseCode').textContent = status;
document.getElementById('responseHeaders').textContent = headers;
document.getElementById('cached').textContent = cached ? '✅ Yes' : '❌ No';
document.getElementById('networkCard').classList.add('active');
}
function formatBytes(bytes) {
if (!bytes) return 'Unknown';
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
}
async function startSmartStream() {
try {
log('🎯 Starting smart stream...', 'info');
updateStatus('Initiating smart stream...');
const startTime = performance.now();
const streamUrl = `${API_BASE}/api/media/stream/${MEDIA_ID}`;
log(`📡 Requesting: ${streamUrl}`, 'info');
// First, check headers with HEAD request (won't trigger transcoding)
const headResponse = await fetch(streamUrl, {
method: 'HEAD'
});
log(`📋 Headers received (status: ${headResponse.status})`, 'info');
// Check what type of streaming will be used
const streamingMethod = headResponse.headers.get('x-streaming-method');
const codec = headResponse.headers.get('x-codec');
log(`🎬 Streaming method: ${streamingMethod}, codec: ${codec}`, 'info');
const endTime = performance.now();
const loadTime = Math.round(endTime - startTime);
// Now make the actual video request based on the streaming method
if (streamingMethod === 'hls-arc-a770') {
// HLS with Intel Arc A770 on-demand segment generation
log(`🎯 Using Intel Arc A770 HLS on-demand transcoding`, 'info');
updateStreamingInfo('HLS', 'H.264 (Arc A770)', 'On-Demand Segments');
updateNetworkInfo('302 → HLS', 'Redirect to Playlist', false);
updatePerformanceInfo(loadTime, 0, null);
// Load video with HLS - Safari will follow redirect automatically
video.src = streamUrl;
updateStatus('Loading HLS with Intel Arc A770 on-demand segments...');
} else if (streamingMethod === 'direct-av1') {
// Direct AV1 streaming
log(`🚀 Direct AV1 streaming`, 'info');
updateStreamingInfo('DIRECT', 'AV1', 'Original File');
updateNetworkInfo('200 OK', 'Range Support', true);
updatePerformanceInfo(loadTime, 0, null);
// Load video directly
video.src = streamUrl;
updateStatus('Streaming AV1 directly (zero transcoding)!');
} else if (streamingMethod === 'hls-arc-a770-playlist' || streamingMethod === 'hls-playlist') {
// Direct HLS playlist response (probably from playlist endpoint)
log(`📺 Direct HLS playlist detected`, 'info');
updateStreamingInfo('HLS', 'H.264 (Arc A770)', 'Direct Playlist');
updateNetworkInfo('200 OK', 'HLS Playlist', true);
updatePerformanceInfo(loadTime, 0, null);
// This is already an HLS playlist, so Safari should handle it
video.src = streamUrl;
updateStatus('Playing HLS with Intel Arc A770 segments...');
} else {
// Unknown method - try direct loading
log(`❓ Unknown streaming method: ${streamingMethod}`, 'warn');
video.src = streamUrl;
updateStatus('Loading video...');
}
// Monitor video loading
video.addEventListener('loadstart', () => {
log('📺 Video loading started...', 'info');
});
video.addEventListener('loadedmetadata', () => {
const duration = Math.round(video.duration);
log(`✅ Video loaded: ${duration}s duration`, 'success');
updateStatus(`Ready to play (${duration}s duration)`);
});
video.addEventListener('error', (e) => {
log(`❌ Video error: ${e.message}`, 'error');
updateStatus('Video playback error');
});
} catch (error) {
log(`❌ Smart stream failed: ${error.message}`, 'error');
updateStatus('Smart streaming failed');
}
}
async function testHLS() {
try {
log('📺 Testing HLS fallback...', 'info');
const hlsUrl = `${API_BASE}/api/media/stream/${MEDIA_ID}/playlist.m3u8`;
log(`📡 Loading HLS: ${hlsUrl}`, 'info');
updateStreamingInfo('HLS', 'H.264 (transcoded)', 'HLS Segments');
updateStatus('Loading HLS playlist...');
video.src = hlsUrl;
video.addEventListener('loadedmetadata', () => {
log('✅ HLS playlist loaded successfully', 'success');
updateStatus('HLS ready (transcoding on-demand)');
});
} catch (error) {
log(`❌ HLS test failed: ${error.message}`, 'error');
updateStatus('HLS test failed');
}
}
// Initialize on page load
window.addEventListener('load', () => {
detectCodecSupport();
log('🎯 Smart streaming test initialized', 'info');
log('💡 Modern browsers get AV1 directly, legacy browsers get HLS with transcoding', 'info');
});
</script>
</body>
</html>

View file

@ -1,29 +0,0 @@
pub async fn send_contact_email(&self, contact: crate::models::ContactEmail) -> Result<()> {
let phone_str = contact.phone.as_deref().unwrap_or("Not provided");
let html_body = format!(
"<h2>New Contact Form Submission</h2>\n\
<p><strong>Name:</strong> {} {}</p>\n\
<p><strong>Email:</strong> {}</p>\n\
<p><strong>Phone:</strong> {}</p>\n\
<h3>Message:</h3>\n\
<p>{}</p>\n",
contact.first_name,
contact.last_name,
contact.email,
phone_str,
contact.message.replace('\n', "<br>")
);
let email = Message::builder()
.from(self.config.from_email.parse()?)
.to(self.config.admin_email.parse()?)
.subject(format!("New Contact Form Submission from {} {}",
contact.first_name, contact.last_name))
.body(html_body)?;
self.transport.send(email).await?;
tracing::info!("Contact form email sent successfully");
Ok(())
}

View file

@ -1,12 +0,0 @@
pub async fn list_pending(
Query(params): Query<PaginationParams>,
State(state): State<AppState>,
) -> Result<Json<ApiResponse<(Vec<PendingEvent>, i64)>>> {
let (events, total) = crate::db::events::list_pending(&state.pool, params.page.unwrap_or(1) as i32, params.per_page.unwrap_or(10)).await?;
Ok(Json(ApiResponse {
success: true,
data: Some((events, total)),
message: None,
}))
}

203
test.sh
View file

@ -1,203 +0,0 @@
#!/bin/bash
echo "🧪 FINAL COMPREHENSIVE API TEST 🧪"
echo "=================================="
PASSED=0
FAILED=0
# Function to test endpoint
test_endpoint() {
local name="$1"
local result="$2"
if [ "$result" = "true" ]; then
echo "$name"
((PASSED++))
else
echo "$name"
((FAILED++))
fi
}
# Get auth token
echo "🔐 Getting authentication token..."
TOKEN=$(curl -s -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}' \
| jq -r '.data.token // empty')
if [ -z "$TOKEN" ]; then
echo "❌ Failed to get auth token!"
exit 1
fi
echo "✅ Auth token obtained"
echo ""
echo "📊 TESTING PUBLIC ENDPOINTS..."
echo "==============================="
# Test public endpoints
test_endpoint "Public Events List" "$(curl -s https://api.rockvilletollandsda.church/api/events | jq -r '.success')"
test_endpoint "Public Bulletins List" "$(curl -s https://api.rockvilletollandsda.church/api/bulletins | jq -r '.success')"
test_endpoint "Public Config" "$(curl -s https://api.rockvilletollandsda.church/api/config | jq -r '.success')"
test_endpoint "Events Upcoming" "$(curl -s https://api.rockvilletollandsda.church/api/events/upcoming | jq -r '.success')"
test_endpoint "Events Featured" "$(curl -s https://api.rockvilletollandsda.church/api/events/featured | jq -r '.success')"
test_endpoint "Current Bulletin" "$(curl -s https://api.rockvilletollandsda.church/api/bulletins/current | jq -r '.success')"
echo ""
echo "🔒 TESTING ADMIN ENDPOINTS..."
echo "============================="
# Test admin endpoints
test_endpoint "Admin Pending Events" "$(curl -s -H "Authorization: Bearer $TOKEN" https://api.rockvilletollandsda.church/api/admin/events/pending | jq -r '.success')"
test_endpoint "Admin Config (with API keys)" "$(curl -s -H "Authorization: Bearer $TOKEN" https://api.rockvilletollandsda.church/api/admin/config | jq -r '.success')"
echo ""
echo "📝 TESTING CRUD OPERATIONS..."
echo "============================="
# Test admin create event
CREATE_RESULT=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Final Test Event",
"description": "Testing complete CRUD",
"start_time": "2025-09-01T18:00:00Z",
"end_time": "2025-09-01T20:00:00Z",
"location": "Test Location",
"category": "Ministry",
"is_featured": false,
"recurring_type": null
}' \
https://api.rockvilletollandsda.church/api/admin/events | jq -r '.success // false')
test_endpoint "Admin Create Event" "$CREATE_RESULT"
if [ "$CREATE_RESULT" = "true" ]; then
# Get the created event ID
EVENT_ID=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Update Test Event",
"description": "Testing update functionality",
"start_time": "2025-09-01T19:00:00Z",
"end_time": "2025-09-01T21:00:00Z",
"location": "Test Location 2",
"category": "Ministry",
"is_featured": false,
"recurring_type": null
}' \
https://api.rockvilletollandsda.church/api/admin/events | jq -r '.data.id // empty')
if [ ! -z "$EVENT_ID" ]; then
# Test update
UPDATE_RESULT=$(curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Updated Test Event",
"description": "Testing update functionality - UPDATED",
"start_time": "2025-09-01T19:30:00Z",
"end_time": "2025-09-01T21:30:00Z",
"location": "Updated Location",
"category": "Ministry",
"is_featured": true,
"recurring_type": null
}' \
https://api.rockvilletollandsda.church/api/admin/events/$EVENT_ID | jq -r '.success // false')
test_endpoint "Admin Update Event" "$UPDATE_RESULT"
# Test delete
DELETE_RESULT=$(curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
https://api.rockvilletollandsda.church/api/admin/events/$EVENT_ID | jq -r '.success // false')
test_endpoint "Admin Delete Event" "$DELETE_RESULT"
else
echo "❌ Could not get event ID for update/delete tests"
((FAILED+=2))
fi
fi
echo ""
echo "📧 TESTING EVENT SUBMISSION & WORKFLOW..."
echo "========================================"
# Test event submission
SUBMIT_RESULT=$(curl -s -X POST https://api.rockvilletollandsda.church/api/events/submit \
-H "Content-Type: application/json" \
-d '{
"title": "Test Submission Workflow",
"description": "Testing the complete submission to approval workflow",
"start_time": "2025-09-15T18:00:00Z",
"end_time": "2025-09-15T20:00:00Z",
"location": "Fellowship Hall",
"category": "Social",
"bulletin_week": "current",
"submitter_email": "admin@rockvilletollandsda.church"
}' | jq -r '.success // false')
test_endpoint "Public Event Submission" "$SUBMIT_RESULT"
echo ""
echo "📁 TESTING FILE UPLOAD..."
echo "========================="
# Test file upload (create a small test file)
echo "Test file content for API testing" > test_upload.txt
BULLETIN_ID=$(curl -s https://api.rockvilletollandsda.church/api/bulletins | jq -r '.data.items[0].id // empty')
if [ ! -z "$BULLETIN_ID" ]; then
UPLOAD_RESULT=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@test_upload.txt" \
https://api.rockvilletollandsda.church/api/upload/bulletins/$BULLETIN_ID/pdf | jq -r '.success // false')
test_endpoint "File Upload" "$UPLOAD_RESULT"
rm -f test_upload.txt
else
echo "❌ Could not get bulletin ID for file upload test"
((FAILED++))
fi
echo ""
echo "🔄 TESTING RECURRING EVENTS..."
echo "=============================="
# Check if recurring events scheduler is running
RECURRING_LOG=$(sudo journalctl -u church-api.service -n 50 | grep -c "recurring events update" || echo "0")
if [ "$RECURRING_LOG" -gt 0 ]; then
echo "✅ Recurring Events Scheduler Running"
((PASSED++))
else
echo "❌ Recurring Events Scheduler Not Found in Logs"
((FAILED++))
fi
echo ""
echo "📊 FINAL RESULTS"
echo "================"
echo "✅ Tests Passed: $PASSED"
echo "❌ Tests Failed: $FAILED"
echo "📈 Success Rate: $(( PASSED * 100 / (PASSED + FAILED) ))%"
if [ $FAILED -eq 0 ]; then
echo ""
echo "🎉🎉🎉 ALL TESTS PASSED! 🎉🎉🎉"
echo "🚀 YOUR CHURCH API IS 100% FUNCTIONAL! 🚀"
echo "💪 READY FOR PRODUCTION! 💪"
else
echo ""
echo "⚠️ Some tests failed. Check the failed endpoints."
fi
echo ""
echo "📋 SUMMARY OF WORKING FEATURES:"
echo "==============================="
echo "🔐 Authentication & User Management"
echo "📊 Complete CRUD Operations"
echo "📧 Email Notifications"
echo "📁 File Upload & Storage"
echo "⚡ Event Approval Workflow"
echo "🗑️ Delete Operations"
echo "🔄 Automatic Recurring Events"
echo "🔧 Secure Configuration Management"
echo "🌐 Public & Admin API Endpoints"

View file

@ -1,38 +0,0 @@
#!/bin/bash
echo "🔍 DEBUGGING FILE UPLOAD ISSUE"
echo "==============================="
# Get token
TOKEN=$(curl -s -X POST https://api.rockvilletollandsda.church/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}' \
| jq -r '.data.token')
# Get bulletin ID
BULLETIN_ID=$(curl -s https://api.rockvilletollandsda.church/api/bulletins | jq -r '.data.items[0].id')
echo "Using bulletin ID: $BULLETIN_ID"
# Create test file
echo "Test PDF content for debugging" > debug_test.pdf
echo ""
echo "Testing file upload with verbose output..."
curl -v -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@debug_test.pdf" \
https://api.rockvilletollandsda.church/api/upload/bulletins/$BULLETIN_ID/pdf
echo ""
echo "Checking service logs for upload errors..."
sudo journalctl -u church-api.service -n 10 | tail -5
echo ""
echo "Testing file serve endpoint..."
curl -I https://api.rockvilletollandsda.church/api/upload/files/test.txt
# Cleanup
rm -f debug_test.pdf
echo ""
echo "Checking upload directory permissions..."
ls -la /opt/rtsda/church-api/uploads/

View file

@ -1,117 +0,0 @@
#!/usr/bin/env fish
echo "🖼️ DIRECT FILE COPY + PROPER UPDATE"
echo "==================================="
set API_BASE "https://api.rockvilletollandsda.church/api"
set STORAGE_PATH "/media/archive/pocketbase-temp/pocketbase/pb_data/storage/2tz9osuik53a0yh"
set OLD_PB_BASE "https://pocketbase.rockvilletollandsda.church/api"
set UPLOAD_DIR "/opt/rtsda/church-api/uploads/events"
# Get token
set AUTH_RESPONSE (curl -s -X POST $API_BASE/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "Alright8-Reapply-Shrewdly-Platter-Important-Keenness-Banking-Streak-Tactile"}')
set JWT_TOKEN (echo $AUTH_RESPONSE | jq -r '.data.token')
echo "✅ Got token"
# Get events for matching
set NEW_EVENTS (curl -s -H "Authorization: Bearer $JWT_TOKEN" "$API_BASE/events?perPage=500")
echo $NEW_EVENTS | jq '.data.items | map({id, title})' > new_events.json
set OLD_EVENTS (curl -s "$OLD_PB_BASE/collections/events/records?perPage=500")
echo $OLD_EVENTS | jq '.items | map({id, title})' > old_events.json
set uploaded 0
set failed 0
for event_dir in (find $STORAGE_PATH -mindepth 1 -maxdepth 1 -type d -name '[a-z0-9]*')
set old_id (basename $event_dir)
set image_file (find $event_dir -maxdepth 1 -name "*.webp" -type f | head -1)
if test -z "$image_file"
continue
end
# Get old event and find new match
set old_event (cat old_events.json | jq --arg id "$old_id" '.[] | select(.id == $id)')
if test -z "$old_event"
continue
end
set title (echo $old_event | jq -r '.title')
set new_event (cat new_events.json | jq --arg title "$title" '.[] | select(.title == $title)')
if test -z "$new_event"
echo "❌ No match for: $title"
continue
end
set new_id (echo $new_event | jq -r '.id')
set filename (basename $image_file)
set new_filename "$new_id-$filename"
set image_path "uploads/events/$new_filename"
echo "📤 Processing: $title"
# Copy file to upload directory
cp "$image_file" "$UPLOAD_DIR/$new_filename"
if test $status -eq 0
echo "✅ File copied: $new_filename"
# Get current event data first
set current_event (curl -s -H "Authorization: Bearer $JWT_TOKEN" \
"$API_BASE/events/$new_id")
# Extract current event data and add image path
set event_data (echo $current_event | jq --arg img "$image_path" \
'.data | {
title: .title,
description: .description,
start_time: .start_time,
end_time: .end_time,
location: .location,
location_url: .location_url,
category: .category,
recurring_type: .recurring_type,
is_featured: .is_featured,
image: $img
}')
# Update event with complete data
set update_response (curl -s -X PUT \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d "$event_data" \
"$API_BASE/admin/events/$new_id")
set success (echo $update_response | jq -r '.success // false')
if test "$success" = "true"
echo "✅ SUCCESS: $title"
set uploaded (math $uploaded + 1)
else
echo "❌ DB UPDATE FAILED: $title"
echo " Response: "(echo $update_response)
set failed (math $failed + 1)
end
else
echo "❌ FILE COPY FAILED: $title"
set failed (math $failed + 1)
end
echo "---"
sleep 0.1
end
rm -f new_events.json old_events.json
echo ""
echo "🎉 FINAL RESULTS!"
echo "================="
echo "✅ Successfully uploaded: $uploaded images"
echo "❌ Failed uploads: $failed images"
echo ""
echo "🌐 Images should be accessible at: https://api.rockvilletollandsda.church/uploads/events/"

View file

@ -1,84 +0,0 @@
#!/bin/bash
echo "🔍 COMPREHENSIVE FILE UPLOAD DEBUG"
echo "=================================="
# From your previous debug, we know the file was uploaded as:
UPLOADED_FILE="03378cc5-8c62-48b6-818e-643588b253ce.pdf"
UPLOAD_DIR="/opt/rtsda/church-api/uploads/bulletins"
API_BASE="https://api.rockvilletollandsda.church"
echo "📁 Step 1: Check if uploaded file actually exists"
echo "Looking for: $UPLOAD_DIR/$UPLOADED_FILE"
if [ -f "$UPLOAD_DIR/$UPLOADED_FILE" ]; then
echo "✅ File exists!"
ls -la "$UPLOAD_DIR/$UPLOADED_FILE"
echo "File size: $(du -h "$UPLOAD_DIR/$UPLOADED_FILE" | cut -f1)"
else
echo "❌ File NOT found!"
echo "Contents of bulletins directory:"
ls -la "$UPLOAD_DIR/"
fi
echo ""
echo "📡 Step 2: Test different file serve URL patterns"
echo "Testing various possible endpoints..."
# Test common patterns for file serving
ENDPOINTS=(
"/uploads/bulletins/$UPLOADED_FILE"
"/api/uploads/bulletins/$UPLOADED_FILE"
"/api/files/bulletins/$UPLOADED_FILE"
"/static/uploads/bulletins/$UPLOADED_FILE"
"/files/bulletins/$UPLOADED_FILE"
"/bulletins/$UPLOADED_FILE"
)
for endpoint in "${ENDPOINTS[@]}"; do
echo "Testing: $API_BASE$endpoint"
response=$(curl -s -o /dev/null -w "%{http_code}" "$API_BASE$endpoint")
echo "Response: $response"
if [ "$response" != "404" ]; then
echo "🎉 FOUND WORKING ENDPOINT: $endpoint"
break
fi
done
echo ""
echo "🔧 Step 3: Check API server configuration"
echo "Looking for static file serving configuration..."
# Check if there's a Rust Cargo.toml or main.rs that might show routing
echo "Checking for Rust project files:"
find /opt/rtsda/church-api -name "Cargo.toml" -o -name "main.rs" -o -name "lib.rs" | head -5
echo ""
echo "🌐 Step 4: Check Caddy configuration"
echo "Caddy reverse proxy might need static file rules..."
if [ -f "/etc/caddy/Caddyfile" ]; then
echo "Found Caddyfile, checking for static file rules:"
grep -n -A5 -B5 "file_server\|root\|static" /etc/caddy/Caddyfile || echo "No static file serving rules found"
else
echo "No Caddyfile found at /etc/caddy/Caddyfile"
echo "Checking other common locations:"
find /etc -name "*caddy*" -type f 2>/dev/null | head -5
fi
echo ""
echo "📋 Step 5: Check API server logs for file access attempts"
echo "Recent logs when accessing files:"
journalctl -u church-api.service --since "10 minutes ago" | tail -20
echo ""
echo "🔍 Step 6: Test with a simple file serve"
echo "Let's see what the API returns when we try to access the file:"
echo "Full response headers and body:"
curl -v "$API_BASE/uploads/bulletins/$UPLOADED_FILE" 2>&1
echo ""
echo "💡 SUMMARY & NEXT STEPS:"
echo "Based on the results above, we need to:"
echo "1. Confirm the file exists and has correct permissions"
echo "2. Find the correct endpoint pattern for serving files"
echo "3. Check if static file serving is configured in your API server"
echo "4. Verify Caddy is properly proxying static file requests"

View file

@ -1,422 +0,0 @@
-- Timezone Migration Validation Script
-- File: validate_timezone_migration.sql
--
-- This script validates that the timezone conversion migration was successful.
-- It compares the migrated UTC times with the original EST times from backup tables.
--
-- Run this script after the migration to verify correctness.
-- ================================
-- VALIDATION OVERVIEW
-- ================================
DO $$
BEGIN
RAISE NOTICE '========================================';
RAISE NOTICE 'TIMEZONE MIGRATION VALIDATION REPORT';
RAISE NOTICE 'Generated at: %', NOW();
RAISE NOTICE '========================================';
END $$;
-- ================================
-- 1. BACKUP TABLE VERIFICATION
-- ================================
DO $$
DECLARE
table_info RECORD;
backup_count INTEGER := 0;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '1. BACKUP TABLE VERIFICATION';
RAISE NOTICE '----------------------------';
FOR table_info IN
SELECT
schemaname,
tablename,
n_tup_ins as row_count
FROM pg_stat_user_tables
WHERE tablename LIKE '%_timezone_backup'
ORDER BY tablename
LOOP
RAISE NOTICE 'Backup table: % (% rows)', table_info.tablename, table_info.row_count;
backup_count := backup_count + 1;
END LOOP;
RAISE NOTICE 'Total backup tables found: %', backup_count;
IF backup_count < 8 THEN
RAISE WARNING 'Expected 8 backup tables, found %. Some backups may be missing.', backup_count;
END IF;
END $$;
-- ================================
-- 2. TIMEZONE OFFSET VALIDATION
-- ================================
-- Check that the migration applied correct timezone offsets
WITH timezone_validation AS (
SELECT
e.id,
e.title,
e.start_time as current_utc,
eb.original_start_time as original_est,
EXTRACT(EPOCH FROM (e.start_time - eb.original_start_time))/3600 as hour_offset,
CASE
WHEN EXTRACT(EPOCH FROM (e.start_time - eb.original_start_time))/3600 BETWEEN 4 AND 5 THEN 'CORRECT'
ELSE 'INCORRECT'
END as validation_status
FROM events e
JOIN events_timezone_backup eb ON e.id = eb.id
WHERE e.start_time IS NOT NULL
AND eb.original_start_time IS NOT NULL
LIMIT 10
)
SELECT
'2. TIMEZONE OFFSET VALIDATION' as section,
'' as spacer
UNION ALL
SELECT
'----------------------------' as section,
'' as spacer
UNION ALL
SELECT
'Sample Event: ' || title as section,
'Offset: ' || ROUND(hour_offset::numeric, 2) || ' hours (' || validation_status || ')' as spacer
FROM timezone_validation;
-- ================================
-- 3. DISPLAY TIME VALIDATION
-- ================================
-- Verify that UTC times display correctly in NY timezone
DO $$
DECLARE
event_record RECORD;
sample_count INTEGER := 0;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '3. DISPLAY TIME VALIDATION';
RAISE NOTICE '---------------------------';
RAISE NOTICE 'Verifying UTC times display correctly in America/New_York timezone:';
RAISE NOTICE '';
FOR event_record IN
SELECT
title,
start_time as utc_time,
start_time AT TIME ZONE 'America/New_York' as ny_display_time
FROM events
WHERE start_time IS NOT NULL
ORDER BY start_time
LIMIT 5
LOOP
sample_count := sample_count + 1;
RAISE NOTICE 'Event: %', event_record.title;
RAISE NOTICE ' UTC Time: %', event_record.utc_time;
RAISE NOTICE ' NY Display: %', event_record.ny_display_time;
RAISE NOTICE '';
END LOOP;
IF sample_count = 0 THEN
RAISE WARNING 'No events found for display time validation.';
END IF;
END $$;
-- ================================
-- 4. MIGRATION STATISTICS
-- ================================
DO $$
DECLARE
events_migrated INTEGER;
pending_migrated INTEGER;
bulletins_migrated INTEGER;
users_migrated INTEGER;
total_records INTEGER;
BEGIN
RAISE NOTICE '4. MIGRATION STATISTICS';
RAISE NOTICE '-----------------------';
-- Count migrated records
SELECT COUNT(*) INTO events_migrated
FROM events
WHERE start_time IS NOT NULL OR end_time IS NOT NULL;
SELECT COUNT(*) INTO pending_migrated
FROM pending_events
WHERE start_time IS NOT NULL OR end_time IS NOT NULL OR submitted_at IS NOT NULL;
SELECT COUNT(*) INTO bulletins_migrated
FROM bulletins
WHERE created_at IS NOT NULL OR updated_at IS NOT NULL;
SELECT COUNT(*) INTO users_migrated
FROM users
WHERE created_at IS NOT NULL OR updated_at IS NOT NULL;
total_records := events_migrated + pending_migrated + bulletins_migrated + users_migrated;
RAISE NOTICE 'Events with timestamps: %', events_migrated;
RAISE NOTICE 'Pending events with timestamps: %', pending_migrated;
RAISE NOTICE 'Bulletins with timestamps: %', bulletins_migrated;
RAISE NOTICE 'Users with timestamps: %', users_migrated;
RAISE NOTICE 'TOTAL RECORDS MIGRATED: %', total_records;
END $$;
-- ================================
-- 5. CONSISTENCY CHECKS
-- ================================
DO $$
DECLARE
inconsistent_count INTEGER;
null_timestamp_count INTEGER;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '5. CONSISTENCY CHECKS';
RAISE NOTICE '---------------------';
-- Check for events where start_time > end_time (potential migration issue)
SELECT COUNT(*) INTO inconsistent_count
FROM events
WHERE start_time IS NOT NULL
AND end_time IS NOT NULL
AND start_time > end_time;
RAISE NOTICE 'Events with start_time > end_time: %', inconsistent_count;
IF inconsistent_count > 0 THEN
RAISE WARNING 'Found % events with inconsistent start/end times!', inconsistent_count;
END IF;
-- Check for NULL timestamps where they shouldn't be
SELECT COUNT(*) INTO null_timestamp_count
FROM events
WHERE (start_time IS NULL OR end_time IS NULL);
RAISE NOTICE 'Events with NULL start/end times: %', null_timestamp_count;
IF null_timestamp_count > 0 THEN
RAISE WARNING 'Found % events with NULL timestamps!', null_timestamp_count;
END IF;
END $$;
-- ================================
-- 6. FUTURE EVENT VALIDATION
-- ================================
-- Check upcoming events to ensure they display correctly
DO $$
DECLARE
future_event RECORD;
future_count INTEGER := 0;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '6. FUTURE EVENT VALIDATION';
RAISE NOTICE '--------------------------';
RAISE NOTICE 'Upcoming events (next 30 days):';
RAISE NOTICE '';
FOR future_event IN
SELECT
title,
start_time AT TIME ZONE 'America/New_York' as ny_time,
EXTRACT(DOW FROM (start_time AT TIME ZONE 'America/New_York')) as day_of_week
FROM events
WHERE start_time > NOW()
AND start_time < (NOW() + INTERVAL '30 days')
ORDER BY start_time
LIMIT 5
LOOP
future_count := future_count + 1;
RAISE NOTICE 'Event: %', future_event.title;
RAISE NOTICE ' NY Time: %', future_event.ny_time;
RAISE NOTICE ' Day of Week: %',
CASE future_event.day_of_week::INTEGER
WHEN 0 THEN 'Sunday'
WHEN 1 THEN 'Monday'
WHEN 2 THEN 'Tuesday'
WHEN 3 THEN 'Wednesday'
WHEN 4 THEN 'Thursday'
WHEN 5 THEN 'Friday'
WHEN 6 THEN 'Saturday'
END;
RAISE NOTICE '';
END LOOP;
IF future_count = 0 THEN
RAISE NOTICE 'No upcoming events found in the next 30 days.';
END IF;
END $$;
-- ================================
-- 7. DAYLIGHT SAVING TIME VALIDATION
-- ================================
-- Check that DST transitions are handled correctly
DO $$
DECLARE
dst_record RECORD;
dst_sample_count INTEGER := 0;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '7. DAYLIGHT SAVING TIME VALIDATION';
RAISE NOTICE '-----------------------------------';
RAISE NOTICE 'Checking DST handling for different times of year:';
RAISE NOTICE '';
-- Sample events from different months to check DST handling
FOR dst_record IN
SELECT
title,
start_time,
start_time AT TIME ZONE 'America/New_York' as ny_time,
EXTRACT(MONTH FROM start_time) as month,
CASE
WHEN EXTRACT(MONTH FROM start_time) IN (11, 12, 1, 2, 3) THEN 'EST (UTC-5)'
ELSE 'EDT (UTC-4)'
END as expected_timezone
FROM events
WHERE start_time IS NOT NULL
ORDER BY EXTRACT(MONTH FROM start_time), start_time
LIMIT 6
LOOP
dst_sample_count := dst_sample_count + 1;
RAISE NOTICE 'Month %: % (Expected: %)',
dst_record.month,
dst_record.title,
dst_record.expected_timezone;
RAISE NOTICE ' UTC: %', dst_record.start_time;
RAISE NOTICE ' NY Time: %', dst_record.ny_time;
RAISE NOTICE '';
END LOOP;
IF dst_sample_count = 0 THEN
RAISE NOTICE 'No events found for DST validation.';
END IF;
END $$;
-- ================================
-- 8. MIGRATION LOG VERIFICATION
-- ================================
DO $$
DECLARE
log_record RECORD;
migration_found BOOLEAN := FALSE;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '8. MIGRATION LOG VERIFICATION';
RAISE NOTICE '-----------------------------';
FOR log_record IN
SELECT
migration_name,
executed_at,
description,
success
FROM migration_log
WHERE migration_name LIKE '%timezone%'
ORDER BY executed_at DESC
LOOP
migration_found := TRUE;
RAISE NOTICE 'Migration: %', log_record.migration_name;
RAISE NOTICE 'Executed: %', log_record.executed_at;
RAISE NOTICE 'Success: %', log_record.success;
RAISE NOTICE 'Description: %', log_record.description;
RAISE NOTICE '';
END LOOP;
IF NOT migration_found THEN
RAISE WARNING 'No timezone migration entries found in migration_log table.';
END IF;
END $$;
-- ================================
-- VALIDATION SUMMARY
-- ================================
DO $$
DECLARE
total_events INTEGER;
total_pending INTEGER;
backup_tables INTEGER;
BEGIN
RAISE NOTICE '========================================';
RAISE NOTICE 'VALIDATION SUMMARY';
RAISE NOTICE '========================================';
SELECT COUNT(*) INTO total_events FROM events WHERE start_time IS NOT NULL;
SELECT COUNT(*) INTO total_pending FROM pending_events WHERE start_time IS NOT NULL;
SELECT COUNT(*) INTO backup_tables FROM information_schema.tables WHERE table_name LIKE '%_timezone_backup';
RAISE NOTICE 'Events validated: %', total_events;
RAISE NOTICE 'Pending events validated: %', total_pending;
RAISE NOTICE 'Backup tables available: %', backup_tables;
RAISE NOTICE '';
RAISE NOTICE 'VALIDATION COMPLETED at %', NOW();
RAISE NOTICE '========================================';
IF backup_tables >= 8 AND total_events > 0 THEN
RAISE NOTICE 'STATUS: Migration validation PASSED';
ELSE
RAISE WARNING 'STATUS: Migration validation issues detected - review above';
END IF;
RAISE NOTICE '========================================';
END $$;
-- ================================
-- RECOMMENDED MANUAL CHECKS
-- ================================
-- These queries should be run manually to spot-check results
SELECT
'-- MANUAL CHECK QUERIES' as info,
'-- Run these queries to manually verify migration results:' as instructions
UNION ALL
SELECT
'-- 1. Compare before/after for specific events:' as info,
$manual1$
SELECT
e.title,
eb.original_start_time as "Before (EST-as-UTC)",
e.start_time as "After (True UTC)",
e.start_time AT TIME ZONE 'America/New_York' as "Display (NY Time)",
EXTRACT(EPOCH FROM (e.start_time - eb.original_start_time))/3600 as "Hour Offset"
FROM events e
JOIN events_timezone_backup eb ON e.id = eb.id
WHERE e.start_time IS NOT NULL
ORDER BY e.start_time
LIMIT 10;
$manual1$ as instructions
UNION ALL
SELECT
'-- 2. Check upcoming events display correctly:' as info,
$manual2$
SELECT
title,
start_time as utc_time,
start_time AT TIME ZONE 'America/New_York' as ny_display_time,
end_time AT TIME ZONE 'America/New_York' as ny_end_time
FROM events
WHERE start_time > NOW()
ORDER BY start_time
LIMIT 10;
$manual2$ as instructions
UNION ALL
SELECT
'-- 3. Verify pending events submission times:' as info,
$manual3$
SELECT
title,
submitted_at as utc_submitted,
submitted_at AT TIME ZONE 'America/New_York' as ny_submitted,
start_time AT TIME ZONE 'America/New_York' as ny_event_time
FROM pending_events
WHERE submitted_at IS NOT NULL
ORDER BY submitted_at DESC
LIMIT 5;
$manual3$ as instructions;

View file

@ -1,102 +0,0 @@
-- First, let's check what data actually contains HTML
SELECT 'Bulletins with HTML tags:' as check_type;
SELECT id, title,
CASE WHEN scripture_reading LIKE '%<%' THEN 'HAS HTML' ELSE 'CLEAN' END as scripture_status,
CASE WHEN sabbath_school LIKE '%<%' THEN 'HAS HTML' ELSE 'CLEAN' END as sabbath_status,
CASE WHEN divine_worship LIKE '%<%' THEN 'HAS HTML' ELSE 'CLEAN' END as worship_status
FROM bulletins
WHERE is_active = true
ORDER BY date DESC
LIMIT 3;
-- Show actual content to see what we're dealing with
SELECT 'Current scripture_reading content:' as content_type;
SELECT substring(scripture_reading, 1, 100) as sample_content
FROM bulletins
WHERE is_active = true
ORDER BY date DESC
LIMIT 1;
-- Now let's clean it more aggressively
UPDATE bulletins
SET scripture_reading =
TRIM(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REGEXP_REPLACE(scripture_reading, '<[^>]*>', '', 'g'),
'&amp;', '&'
),
'&lt;', '<'
),
'&gt;', '>'
),
'&quot;', '"'
),
'&#39;', ''''
),
'&nbsp;', ' '
)
),
sabbath_school =
CASE WHEN sabbath_school IS NOT NULL THEN
TRIM(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REGEXP_REPLACE(sabbath_school, '<[^>]*>', '', 'g'),
'&amp;', '&'
),
'&lt;', '<'
),
'&gt;', '>'
),
'&quot;', '"'
),
'&#39;', ''''
),
'&nbsp;', ' '
)
)
ELSE sabbath_school END,
divine_worship =
CASE WHEN divine_worship IS NOT NULL THEN
TRIM(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REGEXP_REPLACE(divine_worship, '<[^>]*>', '', 'g'),
'&amp;', '&'
),
'&lt;', '<'
),
'&gt;', '>'
),
'&quot;', '"'
),
'&#39;', ''''
),
'&nbsp;', ' '
)
)
ELSE divine_worship END
WHERE scripture_reading LIKE '%<%'
OR sabbath_school LIKE '%<%'
OR divine_worship LIKE '%<%';
-- Verify the cleaning worked
SELECT 'After cleaning - scripture_reading content:' as content_type;
SELECT substring(scripture_reading, 1, 100) as sample_content
FROM bulletins
WHERE is_active = true
ORDER BY date DESC
LIMIT 1;

View file

@ -1,90 +0,0 @@
-- Verification script to check HTML entity cleaning migration results
-- Run this after the migration to verify it worked correctly
-- Check if the cleaning function exists
SELECT
'clean_html_entities function: ' ||
CASE WHEN EXISTS (
SELECT 1 FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.proname = 'clean_html_entities' AND n.nspname = 'public'
) THEN '✓ EXISTS' ELSE '✗ MISSING' END as function_status;
-- Count records that still have HTML entities (should be 0 after migration)
SELECT
'Records with HTML tags in bulletins: ' || COUNT(*) as bulletin_html_tags
FROM bulletins
WHERE
title ~ '<[^>]*>' OR
sabbath_school ~ '<[^>]*>' OR
divine_worship ~ '<[^>]*>' OR
scripture_reading ~ '<[^>]*>' OR
sunset ~ '<[^>]*>';
SELECT
'Records with HTML entities in bulletins: ' || COUNT(*) as bulletin_html_entities
FROM bulletins
WHERE
title ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
sabbath_school ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
divine_worship ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
scripture_reading ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
sunset ~ '&(nbsp|amp|lt|gt|quot|#39);';
SELECT
'Records with HTML tags in events: ' || COUNT(*) as event_html_tags
FROM events
WHERE
title ~ '<[^>]*>' OR
description ~ '<[^>]*>' OR
location ~ '<[^>]*>' OR
location_url ~ '<[^>]*>' OR
approved_from ~ '<[^>]*>';
SELECT
'Records with HTML entities in events: ' || COUNT(*) as event_html_entities
FROM events
WHERE
title ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
description ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
location ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
location_url ~ '&(nbsp|amp|lt|gt|quot|#39);' OR
approved_from ~ '&(nbsp|amp|lt|gt|quot|#39);';
-- Show some sample cleaned data
SELECT
'Sample bulletin titles after cleaning:' as sample_data;
SELECT
SUBSTRING(title, 1, 50) || (CASE WHEN LENGTH(title) > 50 THEN '...' ELSE '' END) as cleaned_titles
FROM bulletins
WHERE title IS NOT NULL
ORDER BY updated_at DESC
LIMIT 5;
SELECT
'Sample event descriptions after cleaning:' as sample_data;
SELECT
SUBSTRING(description, 1, 80) || (CASE WHEN LENGTH(description) > 80 THEN '...' ELSE '' END) as cleaned_descriptions
FROM events
WHERE description IS NOT NULL
ORDER BY updated_at DESC
LIMIT 3;
-- Check when records were last updated (should show recent timestamps if migration ran)
SELECT
'Recently updated bulletins: ' || COUNT(*) || ' (updated in last hour)' as recent_bulletins
FROM bulletins
WHERE updated_at > NOW() - INTERVAL '1 hour';
SELECT
'Recently updated events: ' || COUNT(*) || ' (updated in last hour)' as recent_events
FROM events
WHERE updated_at > NOW() - INTERVAL '1 hour';
-- Summary
SELECT '===============================================' as summary;
SELECT 'MIGRATION VERIFICATION COMPLETE' as summary;
SELECT 'If all HTML tag/entity counts above are 0, the migration was successful!' as summary;
SELECT '===============================================' as summary;

View file

@ -1,195 +0,0 @@
#!/bin/bash
echo "🚀 SETTING UP WEBP CONVERSION"
echo "============================="
# Step 1: Add dependencies
echo "📦 Adding WebP dependencies to Cargo.toml..."
if grep -q 'image = ' Cargo.toml; then
echo "✅ image dependency already exists"
else
echo 'image = "0.24"' >> Cargo.toml
echo "✅ Added image dependency"
fi
if grep -q 'webp = ' Cargo.toml; then
echo "✅ webp dependency already exists"
else
echo 'webp = "0.2"' >> Cargo.toml
echo "✅ Added webp dependency"
fi
# Step 2: Create utils directory if it doesn't exist
echo "📁 Creating utils directory..."
mkdir -p src/utils
# Step 3: Create the WebP conversion module
echo "🔧 Creating WebP conversion module..."
cat > src/utils/images.rs << 'EOF'
use image::ImageFormat;
use std::io::Cursor;
pub async fn convert_to_webp(image_bytes: &[u8]) -> Result<Vec<u8>, String> {
let bytes = image_bytes.to_vec();
tokio::task::spawn_blocking(move || {
let img = image::load_from_memory(&bytes)
.map_err(|e| format!("Failed to load image: {}", e))?;
// Resize if too large (optional optimization)
let img = if img.width() > 1920 || img.height() > 1920 {
img.resize(1920, 1920, image::imageops::FilterType::Lanczos3)
} else {
img
};
let rgb_img = img.to_rgb8();
let encoder = webp::Encoder::from_rgb(&rgb_img, img.width(), img.height());
let webp = encoder.encode(85.0); // 85% quality
Ok(webp.to_vec())
})
.await
.map_err(|e| format!("Task failed: {}", e))?
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_webp_conversion() {
// Create a simple 100x100 red image
let img = image::RgbImage::from_fn(100, 100, |_, _| image::Rgb([255, 0, 0]));
let mut buffer = Vec::new();
// Encode as PNG first
{
let mut cursor = Cursor::new(&mut buffer);
image::DynamicImage::ImageRgb8(img)
.write_to(&mut cursor, ImageFormat::Png)
.expect("Failed to write PNG");
}
// Convert to WebP
let webp_data = convert_to_webp(&buffer).await.expect("WebP conversion failed");
// Verify it's actually WebP data
assert!(webp_data.len() > 0);
assert!(webp_data.starts_with(b"RIFF")); // WebP signature
println!("✅ WebP conversion test passed!");
println!(" Original PNG: {} bytes", buffer.len());
println!(" WebP result: {} bytes", webp_data.len());
println!(" Compression: {:.1}%", (1.0 - webp_data.len() as f64 / buffer.len() as f64) * 100.0);
}
#[tokio::test]
async fn test_invalid_image() {
let fake_data = b"not an image";
let result = convert_to_webp(fake_data).await;
assert!(result.is_err());
println!("✅ Invalid image test passed!");
}
}
EOF
echo "✅ Created src/utils/images.rs"
# Step 4: Update or create utils mod.rs
if [ -f src/utils/mod.rs ]; then
echo "📝 Updating src/utils/mod.rs..."
if ! grep -q "pub mod images;" src/utils/mod.rs; then
echo "pub mod images;" >> src/utils/mod.rs
fi
else
echo "📝 Creating src/utils/mod.rs..."
echo "pub mod images;" > src/utils/mod.rs
fi
echo "✅ Updated utils module"
# Step 5: Update main.rs or lib.rs to include utils
echo "📝 Checking for utils module inclusion..."
main_file=""
if [ -f src/main.rs ]; then
main_file="src/main.rs"
elif [ -f src/lib.rs ]; then
main_file="src/lib.rs"
fi
if [ -n "$main_file" ]; then
if ! grep -q "mod utils;" "$main_file"; then
echo "mod utils;" >> "$main_file"
echo "✅ Added utils module to $main_file"
else
echo "✅ Utils module already included in $main_file"
fi
else
echo "⚠️ Couldn't find main.rs or lib.rs - you'll need to add 'mod utils;' manually"
fi
# Step 6: Build to check for errors
echo "🔨 Building project to verify setup..."
if cargo build; then
echo "✅ Build successful!"
else
echo "❌ Build failed - check the errors above"
exit 1
fi
# Step 7: Run tests
echo "🧪 Running WebP conversion tests..."
if cargo test images::tests -- --nocapture; then
echo "✅ All tests passed!"
else
echo "❌ Tests failed"
exit 1
fi
# Step 8: Create example usage
echo "📄 Creating example usage file..."
cat > webp_example.rs << 'EOF'
// Example usage in your upload handler:
use crate::utils::images::convert_to_webp;
pub async fn handle_image_upload(image_data: Vec<u8>) -> Result<String, String> {
// Save original
let original_path = format!("uploads/original_{}.jpg", uuid::Uuid::new_v4());
tokio::fs::write(&original_path, &image_data).await
.map_err(|e| format!("Failed to save original: {}", e))?;
// Convert to WebP
let webp_data = convert_to_webp(&image_data).await?;
let webp_path = format!("uploads/webp_{}.webp", uuid::Uuid::new_v4());
tokio::fs::write(&webp_data, webp_data).await
.map_err(|e| format!("Failed to save WebP: {}", e))?;
Ok(webp_path)
}
// Or for immediate conversion (slower but simpler):
pub async fn convert_and_save_webp(image_data: Vec<u8>, filename: &str) -> Result<String, String> {
let webp_data = convert_to_webp(&image_data).await?;
let webp_path = format!("uploads/{}.webp", filename);
tokio::fs::write(&webp_path, webp_data).await
.map_err(|e| format!("Failed to save: {}", e))?;
Ok(webp_path)
}
EOF
echo "✅ Created webp_example.rs"
echo ""
echo "🎉 WEBP CONVERSION SETUP COMPLETE!"
echo "=================================="
echo "✅ Dependencies added to Cargo.toml"
echo "✅ WebP conversion module created"
echo "✅ Tests written and passing"
echo "✅ Example usage provided"
echo ""
echo "🔧 Next steps:"
echo "1. Import in your upload handler: use crate::utils::images::convert_to_webp;"
echo "2. Call convert_to_webp(&image_bytes).await in your code"
echo "3. Save the returned Vec<u8> as a .webp file"

View file

@ -1,310 +0,0 @@
#!/bin/bash
echo "🔄 UPDATING EVENTS HANDLER (CORRECT VERSION)"
echo "============================================="
# Step 1: Restore from backup if needed
if [ -f src/handlers/events.rs.backup.* ]; then
echo "📦 Restoring from backup first..."
cp src/handlers/events.rs.backup.* src/handlers/events.rs
echo "✅ Restored"
fi
# Step 2: Add the correct imports
echo "📝 Adding correct imports..."
sed -i '/^use uuid::Uuid;$/a\
\
// New imports for WebP and multipart support\
use axum::extract::Multipart;\
use crate::utils::images::convert_to_webp;\
use tokio::fs;\
use chrono::{DateTime, Utc};' src/handlers/events.rs
echo "✅ Imports added"
# Step 3: Create the CORRECT submit function
echo "🔧 Creating CORRECT submit function..."
cat > /tmp/correct_submit_function.rs << 'EOF'
pub async fn submit(
State(state): State<AppState>,
mut multipart: Multipart,
) -> Result<Json<ApiResponse<PendingEvent>>> {
// Initialize the request struct with ACTUAL fields
let mut req = SubmitEventRequest {
title: String::new(),
description: String::new(),
start_time: Utc::now(), // Temporary default
end_time: Utc::now(), // Temporary default
location: String::new(),
location_url: None,
category: String::new(),
is_featured: None,
recurring_type: None,
bulletin_week: String::new(),
submitter_email: None,
};
// Track image paths (we'll save these separately to DB)
let mut image_path: Option<String> = None;
let mut thumbnail_path: Option<String> = None;
// Extract form fields and files
while let Some(field) = multipart.next_field().await.map_err(|e| {
ApiError::ValidationError(format!("Failed to read multipart field: {}", e))
})? {
let name = field.name().unwrap_or("").to_string();
match name.as_str() {
"title" => {
req.title = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid title: {}", e))
})?;
},
"description" => {
req.description = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid description: {}", e))
})?;
},
"start_time" => {
let time_str = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid start_time: {}", e))
})?;
// Parse as NaiveDateTime first, then convert to UTC
let naive_dt = chrono::NaiveDateTime::parse_from_str(&time_str, "%Y-%m-%dT%H:%M")
.map_err(|e| ApiError::ValidationError(format!("Invalid start_time format: {}", e)))?;
req.start_time = DateTime::from_utc(naive_dt, Utc);
},
"end_time" => {
let time_str = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid end_time: {}", e))
})?;
let naive_dt = chrono::NaiveDateTime::parse_from_str(&time_str, "%Y-%m-%dT%H:%M")
.map_err(|e| ApiError::ValidationError(format!("Invalid end_time format: {}", e)))?;
req.end_time = DateTime::from_utc(naive_dt, Utc);
},
"location" => {
req.location = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid location: {}", e))
})?;
},
"category" => {
req.category = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid category: {}", e))
})?;
},
"location_url" => {
let url = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid location_url: {}", e))
})?;
if !url.is_empty() {
req.location_url = Some(url);
}
},
"reoccuring" => { // Note: form uses "reoccuring" but model uses "recurring_type"
let recurring = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid recurring: {}", e))
})?;
if !recurring.is_empty() {
req.recurring_type = Some(recurring);
}
},
"submitter_email" => {
let email = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid submitter_email: {}", e))
})?;
if !email.is_empty() {
req.submitter_email = Some(email);
}
},
"bulletin_week" => {
req.bulletin_week = field.text().await.map_err(|e| {
ApiError::ValidationError(format!("Invalid bulletin_week: {}", e))
})?;
},
"image" => {
let image_data = field.bytes().await.map_err(|e| {
ApiError::ValidationError(format!("Failed to read image: {}", e))
})?;
if !image_data.is_empty() {
// Save original immediately
let uuid = Uuid::new_v4();
let original_path = format!("uploads/events/original_{}.jpg", uuid);
// Ensure directory exists
fs::create_dir_all("uploads/events").await.map_err(|e| {
ApiError::FileError(e)
})?;
fs::write(&original_path, &image_data).await.map_err(|e| {
ApiError::FileError(e)
})?;
// Set original path immediately
image_path = Some(original_path.clone());
// Convert to WebP in background (user doesn't wait)
let pool = state.pool.clone();
tokio::spawn(async move {
if let Ok(webp_data) = convert_to_webp(&image_data).await {
let webp_path = format!("uploads/events/{}.webp", uuid);
if fs::write(&webp_path, webp_data).await.is_ok() {
// Update database with WebP path (using actual column name "image")
let _ = sqlx::query!(
"UPDATE pending_events SET image = $1 WHERE image = $2",
webp_path,
original_path
).execute(&pool).await;
// Delete original file
let _ = fs::remove_file(&original_path).await;
}
}
});
}
},
"thumbnail" => {
let thumb_data = field.bytes().await.map_err(|e| {
ApiError::ValidationError(format!("Failed to read thumbnail: {}", e))
})?;
if !thumb_data.is_empty() {
let uuid = Uuid::new_v4();
let original_path = format!("uploads/events/thumb_original_{}.jpg", uuid);
fs::create_dir_all("uploads/events").await.map_err(|e| {
ApiError::FileError(e)
})?;
fs::write(&original_path, &thumb_data).await.map_err(|e| {
ApiError::FileError(e)
})?;
thumbnail_path = Some(original_path.clone());
// Convert thumbnail to WebP in background
let pool = state.pool.clone();
tokio::spawn(async move {
if let Ok(webp_data) = convert_to_webp(&thumb_data).await {
let webp_path = format!("uploads/events/thumb_{}.webp", uuid);
if fs::write(&webp_path, webp_data).await.is_ok() {
let _ = sqlx::query!(
"UPDATE pending_events SET thumbnail = $1 WHERE thumbnail = $2",
webp_path,
original_path
).execute(&pool).await;
let _ = fs::remove_file(&original_path).await;
}
}
});
}
},
_ => {
// Ignore unknown fields
let _ = field.bytes().await;
}
}
}
// Validate required fields
if req.title.is_empty() {
return Err(ApiError::ValidationError("Title is required".to_string()));
}
if req.description.is_empty() {
return Err(ApiError::ValidationError("Description is required".to_string()));
}
if req.location.is_empty() {
return Err(ApiError::ValidationError("Location is required".to_string()));
}
if req.category.is_empty() {
return Err(ApiError::ValidationError("Category is required".to_string()));
}
if req.bulletin_week.is_empty() {
req.bulletin_week = "current".to_string(); // Default value
}
// Submit to database first
let mut pending_event = db::events::submit_for_approval(&state.pool, req).await?;
// Update with image paths if we have them
if let Some(img_path) = image_path {
sqlx::query!(
"UPDATE pending_events SET image = $1 WHERE id = $2",
img_path,
pending_event.id
).execute(&state.pool).await.map_err(ApiError::DatabaseError)?;
}
if let Some(thumb_path) = thumbnail_path {
sqlx::query!(
"UPDATE pending_events SET thumbnail = $1 WHERE id = $2",
thumb_path,
pending_event.id
).execute(&state.pool).await.map_err(ApiError::DatabaseError)?;
}
// Send email notification to admin (existing logic)
let mailer = state.mailer.clone();
let event_for_email = pending_event.clone();
tokio::spawn(async move {
if let Err(e) = mailer.send_event_submission_notification(&event_for_email).await {
tracing::error!("Failed to send email: {:?}", e);
} else {
tracing::info!("Email sent for event: {}", event_for_email.title);
}
});
Ok(Json(ApiResponse {
success: true,
data: Some(pending_event),
message: Some("Event submitted successfully! Images are being optimized in the background.".to_string()),
}))
}
EOF
# Step 4: Replace the old submit function with the CORRECT one
echo "🔄 Replacing submit function..."
# Find the line numbers of the current submit function
start_line=$(grep -n "^pub async fn submit(" src/handlers/events.rs | cut -d: -f1)
end_line=$(awk "NR>$start_line && /^}/ {print NR; exit}" src/handlers/events.rs)
if [ -n "$start_line" ] && [ -n "$end_line" ]; then
# Create a temporary file with everything except the old submit function
head -n $((start_line - 1)) src/handlers/events.rs > /tmp/events_before.rs
tail -n +$((end_line + 1)) src/handlers/events.rs > /tmp/events_after.rs
# Combine: before + new function + after
cat /tmp/events_before.rs /tmp/correct_submit_function.rs /tmp/events_after.rs > src/handlers/events.rs
echo "✅ Submit function replaced successfully"
else
echo "❌ Could not find submit function boundaries"
exit 1
fi
# Step 5: Clean up temp files
rm -f /tmp/correct_submit_function.rs /tmp/events_before.rs /tmp/events_after.rs
# Step 6: Build to check for errors
echo "🔨 Building to verify changes..."
if cargo build; then
echo "✅ Build successful!"
echo ""
echo "🎉 EVENTS HANDLER UPDATED SUCCESSFULLY!"
echo "======================================"
echo "✅ Uses ACTUAL model fields"
echo "✅ Proper DateTime handling"
echo "✅ Correct ApiError variants"
echo "✅ Real database columns"
echo "✅ Background WebP conversion"
echo "✅ No hallucinated bullshit"
echo ""
echo "🚀 Ready to handle multipart form submissions with WebP conversion!"
else
echo "❌ Build failed - check errors above"
exit 1
fi