Restructure project and update gitignore
- Flatten directory structure by moving files from astro-church-website/ to root - Remove church-core subdirectory in favor of inline Rust library - Update .gitignore to properly exclude build artifacts, generated files, and dependencies - Add environment variables, IDE files, and coverage reports to gitignore - Include generated CSS files and native bindings in ignore patterns
This commit is contained in:
parent
1013ca0870
commit
91a1bb7a54
25
.gitignore
vendored
25
.gitignore
vendored
|
@ -7,11 +7,16 @@ node_modules/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Astro build output
|
# Astro build output
|
||||||
dist/
|
dist/
|
||||||
.astro/
|
.astro/
|
||||||
|
|
||||||
|
# Generated CSS from Tailwind builds
|
||||||
|
public/css/theme-light.css
|
||||||
|
public/css/theme-dark.css
|
||||||
|
|
||||||
# Compiled binaries and libraries
|
# Compiled binaries and libraries
|
||||||
*.node
|
*.node
|
||||||
*.so
|
*.so
|
||||||
|
@ -31,10 +36,21 @@ RTSDA/
|
||||||
# Build outputs
|
# Build outputs
|
||||||
build/
|
build/
|
||||||
*.log
|
*.log
|
||||||
|
index.cjs
|
||||||
|
index.d.ts
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
# OS generated files
|
# OS generated files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -43,4 +59,11 @@ ehthumbs.db
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
# Cargo build metadata
|
||||||
|
.cargo/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
BIN
.serena/cache/rust/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
BIN
.serena/cache/rust/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
Binary file not shown.
56
.serena/memories/project_overview.md
Normal file
56
.serena/memories/project_overview.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# RTSDA Web Project Overview
|
||||||
|
|
||||||
|
## Project Purpose
|
||||||
|
This is a church website project for Rockville Tolland Seventh-day Adventist Church, featuring:
|
||||||
|
- Event management and submission system
|
||||||
|
- Sermon archive and streaming
|
||||||
|
- Bulletin management
|
||||||
|
- Contact forms
|
||||||
|
- Admin interface
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Frontend**: Astro 5.13.0 with TailwindCSS
|
||||||
|
- **Backend**: Rust-based church-core library with NAPI-RS bindings
|
||||||
|
- **Architecture**: Hybrid - Astro frontend with Rust backend via native bindings
|
||||||
|
- **API**: External API at `https://api.rockvilletollandsda.church/api/`
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
- `astro-church-website/` - Main Astro frontend
|
||||||
|
- `church-core/` - Rust backend library
|
||||||
|
- `axum-church-website/` - Alternative backend (Axum-based)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── astro-church-website/ # Main Astro frontend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # Reusable components
|
||||||
|
│ │ ├── layouts/ # Page layouts
|
||||||
|
│ │ ├── pages/ # Astro pages and API routes
|
||||||
|
│ │ │ ├── api/ # API endpoints
|
||||||
|
│ │ │ ├── events/ # Event-related pages
|
||||||
|
│ │ │ ├── admin/ # Admin interface
|
||||||
|
│ │ │ └── bulletin/ # Bulletin management
|
||||||
|
│ │ └── lib/ # Utilities and bindings
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── Cargo.toml # Rust dependencies for NAPI
|
||||||
|
│ └── build.rs # Build script
|
||||||
|
├── church-core/ # Rust backend library
|
||||||
|
└── axum-church-website/ # Alternative Axum backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
- `npm run dev` - Start development server (localhost:4321)
|
||||||
|
- `npm run build` - Build production site (includes native build)
|
||||||
|
- `npm run build:native` - Build native Rust bindings only
|
||||||
|
- `npm run preview` - Preview built site
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
1. **Event Submission Form** (`/events/submit`) - Users can submit events for approval
|
||||||
|
2. **Admin Interface** (`/admin`) - Event management and approval
|
||||||
|
3. **Bulletin Archive** (`/bulletin`) - Historical bulletins
|
||||||
|
4. **Live Streaming** (`/live`) - Church service streaming
|
||||||
|
5. **Sermon Archive** (`/sermons`) - Past sermons and audio
|
||||||
|
|
||||||
|
## Known Issue
|
||||||
|
There's a database error when users select "2nd 3rd Saturday" in the recurring field of the event submission form, causing a 500 error.
|
71
.serena/memories/suggested_commands.md
Normal file
71
.serena/memories/suggested_commands.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# Suggested Development Commands
|
||||||
|
|
||||||
|
## Core Development Commands
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npm run dev # Starts at localhost:4321
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build # Includes native Rust build
|
||||||
|
npm run build:native # Build only Rust bindings
|
||||||
|
npm run preview # Preview production build
|
||||||
|
|
||||||
|
# Astro CLI commands
|
||||||
|
npm run astro ... # Run any Astro CLI command
|
||||||
|
npm run astro -- --help # Get Astro CLI help
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Utilities (Darwin/macOS)
|
||||||
|
```bash
|
||||||
|
# Basic file operations
|
||||||
|
ls # List files
|
||||||
|
cd <directory> # Change directory
|
||||||
|
find . -name "*.ts" # Find TypeScript files
|
||||||
|
grep -r "pattern" . # Search for patterns
|
||||||
|
|
||||||
|
# Git operations
|
||||||
|
git status # Check git status
|
||||||
|
git add . # Stage all changes
|
||||||
|
git commit -m "message" # Commit changes
|
||||||
|
git push # Push to remote
|
||||||
|
|
||||||
|
# Package management
|
||||||
|
npm install <package> # Install new package
|
||||||
|
npm uninstall <package> # Remove package
|
||||||
|
npm audit # Check for vulnerabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rust/NAPI Commands
|
||||||
|
```bash
|
||||||
|
# Native bindings
|
||||||
|
cargo build --release # Build Rust code
|
||||||
|
napi build --platform --release # Build NAPI bindings
|
||||||
|
|
||||||
|
# Rust development
|
||||||
|
cargo check # Check Rust code
|
||||||
|
cargo test # Run Rust tests
|
||||||
|
cargo clippy # Rust linter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing & Quality (when available)
|
||||||
|
```bash
|
||||||
|
# Check if these exist in project:
|
||||||
|
npm run test # Run tests (if configured)
|
||||||
|
npm run lint # Lint code (if configured)
|
||||||
|
npm run format # Format code (if configured)
|
||||||
|
npm run typecheck # TypeScript checking (if configured)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
```bash
|
||||||
|
# Development debugging
|
||||||
|
npm run dev -- --verbose # Verbose development mode
|
||||||
|
npm run build -- --verbose # Verbose build
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tail -f logs/app.log # If logs exist
|
||||||
|
console logs via browser dev tools # For frontend debugging
|
||||||
|
```
|
52
.serena/memories/task_completion_checklist.md
Normal file
52
.serena/memories/task_completion_checklist.md
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Task Completion Checklist
|
||||||
|
|
||||||
|
When completing any development task in this project, follow these steps:
|
||||||
|
|
||||||
|
## 1. Code Quality Checks
|
||||||
|
- [ ] **TypeScript Validation**: Run `npx tsc --noEmit` to check TypeScript types
|
||||||
|
- [ ] **Build Test**: Run `npm run build` to ensure the project builds successfully
|
||||||
|
- [ ] **Development Server**: Test with `npm run dev` to ensure functionality works locally
|
||||||
|
|
||||||
|
## 2. Rust/Native Code (if applicable)
|
||||||
|
- [ ] **Rust Check**: Run `cargo check` in the church-core directory
|
||||||
|
- [ ] **Native Build**: Run `npm run build:native` to ensure NAPI bindings compile
|
||||||
|
- [ ] **Cargo Clippy**: Run `cargo clippy` for Rust linting (if available)
|
||||||
|
|
||||||
|
## 3. Frontend Testing
|
||||||
|
- [ ] **Manual Testing**: Test all affected functionality in the browser
|
||||||
|
- [ ] **Cross-browser Check**: Test in different browsers if UI changes were made
|
||||||
|
- [ ] **Mobile Responsiveness**: Check mobile layout if UI changes were made
|
||||||
|
|
||||||
|
## 4. API Integration
|
||||||
|
- [ ] **API Endpoints**: Test any modified API routes (`/api/*`)
|
||||||
|
- [ ] **External API**: Verify integration with `https://api.rockvilletollandsda.church/api/`
|
||||||
|
- [ ] **Error Handling**: Ensure proper error responses and user feedback
|
||||||
|
|
||||||
|
## 5. Documentation
|
||||||
|
- [ ] **Code Comments**: Add comments for complex logic
|
||||||
|
- [ ] **README Updates**: Update documentation if new features or requirements
|
||||||
|
- [ ] **Memory Updates**: Update project memory files if architecture changes
|
||||||
|
|
||||||
|
## 6. Git Best Practices
|
||||||
|
- [ ] **Commit Messages**: Use clear, descriptive commit messages
|
||||||
|
- [ ] **Branch Management**: Work on feature branches when appropriate
|
||||||
|
- [ ] **Clean History**: Squash commits if multiple small fixes
|
||||||
|
|
||||||
|
## 7. Performance & Security
|
||||||
|
- [ ] **Bundle Size**: Check if changes affect build size significantly
|
||||||
|
- [ ] **Security**: Ensure no sensitive data is exposed
|
||||||
|
- [ ] **Caching**: Verify API caching headers are appropriate
|
||||||
|
|
||||||
|
## Common Issues to Watch For
|
||||||
|
- **NAPI Bindings**: Ensure native code compiles on target platforms
|
||||||
|
- **External API Dependency**: Handle cases where external API is unavailable
|
||||||
|
- **Image Uploads**: Validate file sizes and types properly
|
||||||
|
- **Form Validation**: Both client-side and server-side validation
|
||||||
|
- **Recurring Events**: Special attention to date/time handling
|
||||||
|
|
||||||
|
## Pre-deployment Checklist
|
||||||
|
- [ ] All TypeScript errors resolved
|
||||||
|
- [ ] Production build succeeds
|
||||||
|
- [ ] All features tested manually
|
||||||
|
- [ ] External dependencies verified
|
||||||
|
- [ ] Environment variables documented (if new ones added)
|
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# 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: "rtsda-web-main"
|
|
@ -9,7 +9,7 @@ crate-type = ["cdylib"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
napi = { version = "2", default-features = false, features = ["napi4"] }
|
napi = { version = "2", default-features = false, features = ["napi4"] }
|
||||||
napi-derive = "2"
|
napi-derive = "2"
|
||||||
church-core = { path = "../church-core" }
|
church-core = { git = "https://git.rockvilletollandsda.church/RTSDA/church-core.git" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
119
README.md
119
README.md
|
@ -1,104 +1,43 @@
|
||||||
# RTSDA Website
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
The official website for Rockville Tolland Seventh-day Adventist Church, built with Astro and powered by Rust bindings.
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Modern Astro Framework** - Fast, component-based architecture
|
|
||||||
- **Event Management** - Submit and manage church events with recurring types
|
|
||||||
- **Live Streaming** - Watch services live with HLS.js support
|
|
||||||
- **Mobile App Integration** - iOS and Android app download links
|
|
||||||
- **Admin Panel** - Manage events, bulletins, and church content
|
|
||||||
- **Three Angels' Message** - Dedicated sections for Adventist theology
|
|
||||||
- **Rust-Powered Backend** - High-performance API bindings via `church-core`
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
astro-church-website/ # Frontend Astro application
|
|
||||||
├── src/
|
|
||||||
│ ├── pages/ # Astro pages and API routes
|
|
||||||
│ ├── components/ # Reusable UI components
|
|
||||||
│ └── layouts/ # Page layouts
|
|
||||||
└── public/ # Static assets
|
|
||||||
|
|
||||||
church-core/ # Rust library for API bindings
|
|
||||||
├── src/
|
|
||||||
│ ├── client/ # API client implementations
|
|
||||||
│ ├── models/ # Data structures
|
|
||||||
│ └── uniffi_wrapper.rs # FFI bindings
|
|
||||||
└── Cargo.toml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
### Prerequisites
|
## 🚀 Project Structure
|
||||||
|
|
||||||
- Node.js 18+ and npm
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
- Rust 1.70+
|
|
||||||
- Cargo
|
|
||||||
|
|
||||||
### Setup
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
1. **Install dependencies:**
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
```bash
|
|
||||||
cd astro-church-website
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Build Rust bindings:**
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
```bash
|
|
||||||
npm run build:native
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Start development server:**
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building for Production
|
## 🧞 Commands
|
||||||
|
|
||||||
1. **Build native bindings:**
|
All commands are run from the root of the project, from a terminal:
|
||||||
```bash
|
|
||||||
npm run build:native
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Build Astro site:**
|
| Command | Action |
|
||||||
```bash
|
| :------------------------ | :----------------------------------------------- |
|
||||||
npm run build
|
| `npm install` | Installs dependencies |
|
||||||
```
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
3. **Deploy to web server:**
|
## 👀 Want to learn more?
|
||||||
```bash
|
|
||||||
cp -r dist/* /var/www/rtsda-website/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Recent Fixes
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
|
|
||||||
- **SecondThirdSaturday Recurring Type** - Added support for `"2nd/3rd Saturday Monthly"` events
|
|
||||||
- **Event Display Issues** - Fixed events page showing "No Events Scheduled"
|
|
||||||
- **iOS App Compatibility** - Resolved recurring type parsing errors
|
|
||||||
- **Mobile App Downloads** - Added iOS App Store and Android APK download buttons
|
|
||||||
- **Session Management** - Improved admin panel authentication handling
|
|
||||||
|
|
||||||
## API Integration
|
|
||||||
|
|
||||||
The site integrates with the church API at `https://api.rockvilletollandsda.church` for:
|
|
||||||
|
|
||||||
- Events and recurring schedules
|
|
||||||
- Sermon archives and live streams
|
|
||||||
- Church bulletins and announcements
|
|
||||||
- Contact form submissions
|
|
||||||
- Admin authentication
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Make your changes
|
|
||||||
4. Test thoroughly
|
|
||||||
5. Submit a pull request
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
See LICENSE file for details.
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
[target.x86_64-unknown-linux-gnu]
|
|
||||||
linker = "x86_64-linux-gnu-gcc"
|
|
||||||
|
|
||||||
[env]
|
|
||||||
CC_x86_64_unknown_linux_gnu = "x86_64-linux-gnu-gcc"
|
|
||||||
CXX_x86_64_unknown_linux_gnu = "x86_64-linux-gnu-g++"
|
|
||||||
AR_x86_64_unknown_linux_gnu = "x86_64-linux-gnu-ar"
|
|
||||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "x86_64-linux-gnu-gcc"
|
|
24
astro-church-website/.gitignore
vendored
24
astro-church-website/.gitignore
vendored
|
@ -1,24 +0,0 @@
|
||||||
# build output
|
|
||||||
dist/
|
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# jetbrains setting folder
|
|
||||||
.idea/
|
|
|
@ -1,43 +0,0 @@
|
||||||
# Astro Starter Kit: Minimal
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm create astro@latest -- --template minimal
|
|
||||||
```
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
├── src/
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
|
||||||
|
|
||||||
## 🧞 Commands
|
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `npm install` | Installs dependencies |
|
|
||||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
|
||||||
|
|
||||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
|
|
@ -1,343 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
/* auto-generated by NAPI-RS */
|
|
||||||
|
|
||||||
const { existsSync, readFileSync } = require('fs')
|
|
||||||
const { join } = require('path')
|
|
||||||
|
|
||||||
const { platform, arch } = process
|
|
||||||
|
|
||||||
let nativeBinding = null
|
|
||||||
let localFileExisted = false
|
|
||||||
let loadError = null
|
|
||||||
|
|
||||||
function isMusl() {
|
|
||||||
// For Node 10
|
|
||||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
|
||||||
try {
|
|
||||||
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
|
||||||
return readFileSync(lddPath, 'utf8').includes('musl')
|
|
||||||
} catch (e) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { glibcVersionRuntime } = process.report.getReport().header
|
|
||||||
return !glibcVersionRuntime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case 'android':
|
|
||||||
switch (arch) {
|
|
||||||
case 'arm64':
|
|
||||||
localFileExisted = existsSync(join(__dirname, 'church-core-bindings.android-arm64.node'))
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.android-arm64.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-android-arm64')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'arm':
|
|
||||||
localFileExisted = existsSync(join(__dirname, 'church-core-bindings.android-arm-eabi.node'))
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.android-arm-eabi.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-android-arm-eabi')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'win32':
|
|
||||||
switch (arch) {
|
|
||||||
case 'x64':
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.win32-x64-msvc.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.win32-x64-msvc.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-win32-x64-msvc')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'ia32':
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.win32-ia32-msvc.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.win32-ia32-msvc.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-win32-ia32-msvc')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'arm64':
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.win32-arm64-msvc.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.win32-arm64-msvc.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-win32-arm64-msvc')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'darwin':
|
|
||||||
localFileExisted = existsSync(join(__dirname, 'church-core-bindings.darwin-universal.node'))
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.darwin-universal.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-darwin-universal')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} catch {}
|
|
||||||
switch (arch) {
|
|
||||||
case 'x64':
|
|
||||||
localFileExisted = existsSync(join(__dirname, 'church-core-bindings.darwin-x64.node'))
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.darwin-x64.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-darwin-x64')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'arm64':
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.darwin-arm64.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.darwin-arm64.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-darwin-arm64')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'freebsd':
|
|
||||||
if (arch !== 'x64') {
|
|
||||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
|
||||||
}
|
|
||||||
localFileExisted = existsSync(join(__dirname, 'church-core-bindings.freebsd-x64.node'))
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.freebsd-x64.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-freebsd-x64')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'linux':
|
|
||||||
switch (arch) {
|
|
||||||
case 'x64':
|
|
||||||
if (isMusl()) {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-x64-musl.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-x64-musl.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-x64-musl')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-x64-gnu.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-x64-gnu.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-x64-gnu')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'arm64':
|
|
||||||
if (isMusl()) {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-arm64-musl.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-arm64-musl.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-arm64-musl')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-arm64-gnu.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-arm64-gnu.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-arm64-gnu')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'arm':
|
|
||||||
if (isMusl()) {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-arm-musleabihf.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-arm-musleabihf.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-arm-musleabihf')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-arm-gnueabihf.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-arm-gnueabihf.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-arm-gnueabihf')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'riscv64':
|
|
||||||
if (isMusl()) {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-riscv64-musl.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-riscv64-musl.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-riscv64-musl')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-riscv64-gnu.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-riscv64-gnu.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-riscv64-gnu')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 's390x':
|
|
||||||
localFileExisted = existsSync(
|
|
||||||
join(__dirname, 'church-core-bindings.linux-s390x-gnu.node')
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
if (localFileExisted) {
|
|
||||||
nativeBinding = require('./church-core-bindings.linux-s390x-gnu.node')
|
|
||||||
} else {
|
|
||||||
nativeBinding = require('astro-church-website-linux-s390x-gnu')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
loadError = e
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nativeBinding) {
|
|
||||||
if (loadError) {
|
|
||||||
throw loadError
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to load native binding`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getChurchName, fetchEventsJson, fetchFeaturedEventsJson, fetchSermonsJson, fetchConfigJson, getMissionStatement, fetchRandomBibleVerseJson, getStreamLiveStatus, getLivestreamUrl, getChurchAddress, getChurchPhysicalAddress, getChurchPoBox, getContactPhone, getContactEmail, getFacebookUrl, getYoutubeUrl, getInstagramUrl, submitContactV2Json, validateContactFormJson, fetchLivestreamArchiveJson, fetchBulletinsJson, fetchCurrentBulletinJson, fetchBibleVerseJson, submitEventJson, testAdminFunction, fetchAllSchedulesJson, createScheduleJson, updateScheduleJson, deleteScheduleJson } = nativeBinding
|
|
||||||
|
|
||||||
module.exports.getChurchName = getChurchName
|
|
||||||
module.exports.fetchEventsJson = fetchEventsJson
|
|
||||||
module.exports.fetchFeaturedEventsJson = fetchFeaturedEventsJson
|
|
||||||
module.exports.fetchSermonsJson = fetchSermonsJson
|
|
||||||
module.exports.fetchConfigJson = fetchConfigJson
|
|
||||||
module.exports.getMissionStatement = getMissionStatement
|
|
||||||
module.exports.fetchRandomBibleVerseJson = fetchRandomBibleVerseJson
|
|
||||||
module.exports.getStreamLiveStatus = getStreamLiveStatus
|
|
||||||
module.exports.getLivestreamUrl = getLivestreamUrl
|
|
||||||
module.exports.getChurchAddress = getChurchAddress
|
|
||||||
module.exports.getChurchPhysicalAddress = getChurchPhysicalAddress
|
|
||||||
module.exports.getChurchPoBox = getChurchPoBox
|
|
||||||
module.exports.getContactPhone = getContactPhone
|
|
||||||
module.exports.getContactEmail = getContactEmail
|
|
||||||
module.exports.getFacebookUrl = getFacebookUrl
|
|
||||||
module.exports.getYoutubeUrl = getYoutubeUrl
|
|
||||||
module.exports.getInstagramUrl = getInstagramUrl
|
|
||||||
module.exports.submitContactV2Json = submitContactV2Json
|
|
||||||
module.exports.validateContactFormJson = validateContactFormJson
|
|
||||||
module.exports.fetchLivestreamArchiveJson = fetchLivestreamArchiveJson
|
|
||||||
module.exports.fetchBulletinsJson = fetchBulletinsJson
|
|
||||||
module.exports.fetchCurrentBulletinJson = fetchCurrentBulletinJson
|
|
||||||
module.exports.fetchBibleVerseJson = fetchBibleVerseJson
|
|
||||||
module.exports.submitEventJson = submitEventJson
|
|
||||||
module.exports.testAdminFunction = testAdminFunction
|
|
||||||
module.exports.fetchAllSchedulesJson = fetchAllSchedulesJson
|
|
||||||
module.exports.createScheduleJson = createScheduleJson
|
|
||||||
module.exports.updateScheduleJson = updateScheduleJson
|
|
||||||
module.exports.deleteScheduleJson = deleteScheduleJson
|
|
34
astro-church-website/index.d.ts
vendored
34
astro-church-website/index.d.ts
vendored
|
@ -1,34 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
/* auto-generated by NAPI-RS */
|
|
||||||
|
|
||||||
export declare function getChurchName(): string
|
|
||||||
export declare function fetchEventsJson(): string
|
|
||||||
export declare function fetchFeaturedEventsJson(): string
|
|
||||||
export declare function fetchSermonsJson(): string
|
|
||||||
export declare function fetchConfigJson(): string
|
|
||||||
export declare function getMissionStatement(): string
|
|
||||||
export declare function fetchRandomBibleVerseJson(): string
|
|
||||||
export declare function getStreamLiveStatus(): boolean
|
|
||||||
export declare function getLivestreamUrl(): string
|
|
||||||
export declare function getChurchAddress(): string
|
|
||||||
export declare function getChurchPhysicalAddress(): string
|
|
||||||
export declare function getChurchPoBox(): string
|
|
||||||
export declare function getContactPhone(): string
|
|
||||||
export declare function getContactEmail(): string
|
|
||||||
export declare function getFacebookUrl(): string
|
|
||||||
export declare function getYoutubeUrl(): string
|
|
||||||
export declare function getInstagramUrl(): string
|
|
||||||
export declare function submitContactV2Json(name: string, email: string, subject: string, message: string, phone: string): string
|
|
||||||
export declare function validateContactFormJson(formJson: string): string
|
|
||||||
export declare function fetchLivestreamArchiveJson(): string
|
|
||||||
export declare function fetchBulletinsJson(): string
|
|
||||||
export declare function fetchCurrentBulletinJson(): string
|
|
||||||
export declare function fetchBibleVerseJson(query: string): string
|
|
||||||
export declare function submitEventJson(title: string, description: string, startTime: string, endTime: string, location: string, locationUrl: string | undefined | null, category: string, recurringType?: string | undefined | null, submitterEmail?: string | undefined | null): string
|
|
||||||
export declare function testAdminFunction(): string
|
|
||||||
export declare function fetchAllSchedulesJson(): string
|
|
||||||
export declare function createScheduleJson(scheduleJson: string): string
|
|
||||||
export declare function updateScheduleJson(date: string, updateJson: string): string
|
|
||||||
export declare function deleteScheduleJson(date: string): string
|
|
27
astro-church-website/linux/index.d.ts
vendored
27
astro-church-website/linux/index.d.ts
vendored
|
@ -1,27 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
/* auto-generated by NAPI-RS */
|
|
||||||
|
|
||||||
export declare function getChurchName(): string
|
|
||||||
export declare function fetchEventsJson(): string
|
|
||||||
export declare function fetchFeaturedEventsJson(): string
|
|
||||||
export declare function fetchSermonsJson(): string
|
|
||||||
export declare function fetchConfigJson(): string
|
|
||||||
export declare function getMissionStatement(): string
|
|
||||||
export declare function fetchRandomBibleVerseJson(): string
|
|
||||||
export declare function getStreamLiveStatus(): boolean
|
|
||||||
export declare function getLivestreamUrl(): string
|
|
||||||
export declare function getChurchAddress(): string
|
|
||||||
export declare function getContactPhone(): string
|
|
||||||
export declare function getContactEmail(): string
|
|
||||||
export declare function getFacebookUrl(): string
|
|
||||||
export declare function getYoutubeUrl(): string
|
|
||||||
export declare function getInstagramUrl(): string
|
|
||||||
export declare function submitContactV2Json(name: string, email: string, subject: string, message: string, phone: string): string
|
|
||||||
export declare function validateContactFormJson(formJson: string): string
|
|
||||||
export declare function fetchLivestreamArchiveJson(): string
|
|
||||||
export declare function fetchBulletinsJson(): string
|
|
||||||
export declare function fetchCurrentBulletinJson(): string
|
|
||||||
export declare function fetchBibleVerseJson(query: string): string
|
|
||||||
export declare function submitEventJson(title: string, description: string, startTime: string, endTime: string, location: string, locationUrl: string | undefined | null, category: string, recurringType?: string | undefined | null, submitterEmail?: string | undefined | null): string
|
|
6752
astro-church-website/package-lock.json
generated
6752
astro-church-website/package-lock.json
generated
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,99 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "church-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
description = "Shared Rust crate for church application APIs and data models"
|
|
||||||
authors = ["Benjamin Slingo <benjamin@example.com>"]
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# HTTP client (using rustls to avoid OpenSSL cross-compilation issues)
|
|
||||||
reqwest = { version = "0.11", features = ["json", "multipart", "stream", "rustls-tls"], default-features = false }
|
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
|
||||||
|
|
||||||
# JSON handling
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
|
|
||||||
# Date/time handling
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
|
|
||||||
# Error handling
|
|
||||||
thiserror = "1.0"
|
|
||||||
anyhow = "1.0"
|
|
||||||
|
|
||||||
# Caching and utilities
|
|
||||||
moka = { version = "0.12", features = ["future"] }
|
|
||||||
async-trait = "0.1"
|
|
||||||
rand = "0.8"
|
|
||||||
urlencoding = "2.1"
|
|
||||||
|
|
||||||
# UUID generation
|
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
|
||||||
|
|
||||||
# Base64 encoding for image caching
|
|
||||||
base64 = "0.21"
|
|
||||||
|
|
||||||
# URL handling
|
|
||||||
url = "2.4"
|
|
||||||
|
|
||||||
# Regular expressions
|
|
||||||
regex = "1.10"
|
|
||||||
|
|
||||||
# System calls for iOS device detection
|
|
||||||
libc = "0.2"
|
|
||||||
|
|
||||||
# HTML processing
|
|
||||||
html2text = "0.12"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Testing dependencies
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio-test = "0.4"
|
|
||||||
mockito = "0.31"
|
|
||||||
serde_json = "1.0"
|
|
||||||
tempfile = "3.8"
|
|
||||||
pretty_assertions = "1.4"
|
|
||||||
|
|
||||||
# Optional FFI support
|
|
||||||
[dependencies.wasm-bindgen]
|
|
||||||
version = "0.2"
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[dependencies.wasm-bindgen-futures]
|
|
||||||
version = "0.4"
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[dependencies.js-sys]
|
|
||||||
version = "0.3"
|
|
||||||
optional = true
|
|
||||||
|
|
||||||
[dependencies.web-sys]
|
|
||||||
version = "0.3"
|
|
||||||
optional = true
|
|
||||||
features = [
|
|
||||||
"console",
|
|
||||||
"Window",
|
|
||||||
"Document",
|
|
||||||
"Element",
|
|
||||||
"HtmlElement",
|
|
||||||
"Storage",
|
|
||||||
"Request",
|
|
||||||
"RequestInit",
|
|
||||||
"Response",
|
|
||||||
"Headers",
|
|
||||||
]
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["native"]
|
|
||||||
native = []
|
|
||||||
wasm = ["wasm-bindgen", "wasm-bindgen-futures", "js-sys", "web-sys"]
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "church-core-test"
|
|
||||||
path = "src/bin/test.rs"
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
// No build steps needed
|
|
||||||
}
|
|
|
@ -1,464 +0,0 @@
|
||||||
use crate::{
|
|
||||||
ChurchApiClient, ChurchCoreConfig,
|
|
||||||
models::{NewSchedule, ScheduleUpdate, NewBulletin, BulletinUpdate, NewEvent, EventUpdate},
|
|
||||||
};
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
static CLIENT: OnceLock<ChurchApiClient> = OnceLock::new();
|
|
||||||
static RT: OnceLock<Runtime> = OnceLock::new();
|
|
||||||
|
|
||||||
fn get_client() -> &'static ChurchApiClient {
|
|
||||||
CLIENT.get_or_init(|| {
|
|
||||||
let config = ChurchCoreConfig::default();
|
|
||||||
ChurchApiClient::new(config).expect("Failed to create church client")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_runtime() -> &'static Runtime {
|
|
||||||
RT.get_or_init(|| {
|
|
||||||
Runtime::new().expect("Failed to create async runtime")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration functions
|
|
||||||
pub fn get_church_name() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.church_name.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_contact_phone() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.contact_phone.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_contact_email() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.contact_email.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_church_address() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.church_address.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_church_physical_address() -> String {
|
|
||||||
get_church_address()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_church_po_box() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.po_box.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mission_statement() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.mission_statement.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_facebook_url() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.facebook_url.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_youtube_url() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.youtube_url.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_instagram_url() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => config.instagram_url.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_stream_live_status() -> bool {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_stream_status()) {
|
|
||||||
Ok(status) => status.is_live,
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_livestream_url() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_live_stream()) {
|
|
||||||
Ok(stream) => stream.stream_title.unwrap_or_else(|| "".to_string()),
|
|
||||||
Err(e) => format!("Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON API functions
|
|
||||||
pub fn fetch_events_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_upcoming_events(Some(50))) {
|
|
||||||
Ok(events) => serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_featured_events_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_featured_events_v2(Some(10))) {
|
|
||||||
Ok(events) => serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_sermons_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_recent_sermons(Some(20))) {
|
|
||||||
Ok(sermons) => serde_json::to_string(&sermons).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_config_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_config()) {
|
|
||||||
Ok(config) => serde_json::to_string(&config).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(_) => "{}".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_random_bible_verse_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_random_verse()) {
|
|
||||||
Ok(verse) => serde_json::to_string(&verse).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(_) => "{}".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_bulletins_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_bulletins(true)) {
|
|
||||||
Ok(bulletins) => serde_json::to_string(&bulletins).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_current_bulletin_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_current_bulletin()) {
|
|
||||||
Ok(Some(bulletin)) => serde_json::to_string(&bulletin).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Ok(None) => "{}".to_string(),
|
|
||||||
Err(_) => "{}".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_bible_verse_json(query: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_verse_by_reference(&query)) {
|
|
||||||
Ok(Some(verse)) => serde_json::to_string(&verse).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Ok(None) => "{}".to_string(),
|
|
||||||
Err(_) => "{}".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_livestream_archive_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_livestreams()) {
|
|
||||||
Ok(streams) => serde_json::to_string(&streams).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_contact_v2_json(name: String, email: String, subject: String, message: String, phone: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
let contact = crate::models::ContactForm::new(name, email, subject, message)
|
|
||||||
.with_phone(phone);
|
|
||||||
|
|
||||||
match rt.block_on(client.submit_contact_form_v2(contact)) {
|
|
||||||
Ok(id) => serde_json::to_string(&serde_json::json!({"success": true, "id": id})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_contact_form_json(form_json: String) -> String {
|
|
||||||
match serde_json::from_str::<crate::models::ContactForm>(&form_json) {
|
|
||||||
Ok(_) => serde_json::to_string(&crate::utils::ValidationResult::valid()).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(_) => serde_json::to_string(&crate::utils::ValidationResult::invalid(vec!["Invalid JSON format".to_string()])).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn submit_event_json(
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
start_time: String,
|
|
||||||
end_time: String,
|
|
||||||
location: String,
|
|
||||||
location_url: Option<String>,
|
|
||||||
category: String,
|
|
||||||
recurring_type: Option<String>,
|
|
||||||
submitter_email: Option<String>
|
|
||||||
) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
let submission = crate::models::EventSubmission {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
location,
|
|
||||||
location_url,
|
|
||||||
category,
|
|
||||||
recurring_type,
|
|
||||||
submitter_email: submitter_email.unwrap_or_else(|| "".to_string()),
|
|
||||||
is_featured: false,
|
|
||||||
bulletin_week: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match rt.block_on(client.submit_event(submission)) {
|
|
||||||
Ok(id) => serde_json::to_string(&serde_json::json!({"success": true, "id": id})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin functions
|
|
||||||
pub fn fetch_all_schedules_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.get_all_admin_schedules()) {
|
|
||||||
Ok(schedules) => serde_json::to_string(&schedules).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_schedule_json(schedule_json: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match serde_json::from_str::<NewSchedule>(&schedule_json) {
|
|
||||||
Ok(schedule) => {
|
|
||||||
match rt.block_on(client.create_admin_schedule(schedule)) {
|
|
||||||
Ok(id) => serde_json::to_string(&serde_json::json!({"success": true, "id": id})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": format!("Invalid JSON: {}", e)})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_schedule_json(date: String, update_json: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match serde_json::from_str::<ScheduleUpdate>(&update_json) {
|
|
||||||
Ok(update) => {
|
|
||||||
match rt.block_on(client.update_admin_schedule(&date, update)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": format!("Invalid JSON: {}", e)})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_schedule_json(date: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.delete_admin_schedule(&date)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Auth Functions
|
|
||||||
pub fn admin_login_json(email: String, password: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.admin_login(&email, &password)) {
|
|
||||||
Ok(token) => serde_json::to_string(&serde_json::json!({"success": true, "token": token})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_admin_token_json(token: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(client.validate_admin_token(&token)) {
|
|
||||||
Ok(valid) => serde_json::to_string(&serde_json::json!({"success": true, "valid": valid})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Events Functions
|
|
||||||
pub fn fetch_pending_events_json() -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(crate::client::admin::get_pending_events(client)) {
|
|
||||||
Ok(events) => serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string()),
|
|
||||||
Err(_) => "[]".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn approve_pending_event_json(event_id: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(crate::client::admin::approve_pending_event(client, &event_id)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reject_pending_event_json(event_id: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(crate::client::admin::reject_pending_event(client, &event_id)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_pending_event_json(event_id: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(crate::client::admin::delete_pending_event(client, &event_id)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn update_admin_event_json(event_id: String, update_json: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match serde_json::from_str::<EventUpdate>(&update_json) {
|
|
||||||
Ok(update) => {
|
|
||||||
match rt.block_on(crate::client::admin::update_admin_event(client, &event_id, update)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": format!("Invalid JSON: {}", e)})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_admin_event_json(event_id: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(crate::client::admin::delete_admin_event(client, &event_id)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Bulletins Functions
|
|
||||||
pub fn create_bulletin_json(bulletin_json: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match serde_json::from_str::<NewBulletin>(&bulletin_json) {
|
|
||||||
Ok(bulletin) => {
|
|
||||||
match rt.block_on(crate::client::admin::create_bulletin(client, bulletin)) {
|
|
||||||
Ok(id) => serde_json::to_string(&serde_json::json!({"success": true, "id": id})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": format!("Invalid JSON: {}", e)})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_bulletin_json(bulletin_id: String, update_json: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match serde_json::from_str::<BulletinUpdate>(&update_json) {
|
|
||||||
Ok(update) => {
|
|
||||||
match rt.block_on(crate::client::admin::update_bulletin(client, &bulletin_id, update)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": format!("Invalid JSON: {}", e)})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_bulletin_json(bulletin_id: String) -> String {
|
|
||||||
let client = get_client();
|
|
||||||
let rt = get_runtime();
|
|
||||||
|
|
||||||
match rt.block_on(crate::client::admin::delete_bulletin(client, &bulletin_id)) {
|
|
||||||
Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| "{}".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
// Authentication modules placeholder
|
|
||||||
// This contains authentication implementations
|
|
||||||
|
|
||||||
pub use crate::models::AuthToken;
|
|
|
@ -1,36 +0,0 @@
|
||||||
use church_core::{
|
|
||||||
client::{ChurchApiClient, events::submit_event},
|
|
||||||
models::EventSubmission,
|
|
||||||
config::ChurchCoreConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let config = ChurchCoreConfig::new();
|
|
||||||
let client = ChurchApiClient::new(config).unwrap();
|
|
||||||
|
|
||||||
let submission = EventSubmission {
|
|
||||||
title: "Test Event".to_string(),
|
|
||||||
description: "Testing date submission".to_string(),
|
|
||||||
start_time: "2025-06-28T23:00".to_string(), // The problematic format
|
|
||||||
end_time: "2025-06-29T00:00".to_string(),
|
|
||||||
location: "Test Location".to_string(),
|
|
||||||
location_url: None,
|
|
||||||
category: "Other".to_string(),
|
|
||||||
is_featured: false,
|
|
||||||
recurring_type: None,
|
|
||||||
bulletin_week: None,
|
|
||||||
submitter_email: "test@example.com".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("Testing date validation:");
|
|
||||||
println!("Can parse start_time: {}", submission.parse_start_time().is_some());
|
|
||||||
println!("Can parse end_time: {}", submission.parse_end_time().is_some());
|
|
||||||
println!("Validation passes: {}", submission.validate_times());
|
|
||||||
|
|
||||||
println!("\nAttempting to submit event...");
|
|
||||||
match submit_event(&client, submission).await {
|
|
||||||
Ok(id) => println!("✅ Success! Event ID: {}", id),
|
|
||||||
Err(e) => println!("❌ Error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
use church_core::{ChurchApiClient, ChurchCoreConfig, DeviceCapabilities, StreamingCapability};
|
|
||||||
use chrono::TimeZone;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// Initialize the client with default configuration
|
|
||||||
let config = ChurchCoreConfig::default();
|
|
||||||
let client = ChurchApiClient::new(config)?;
|
|
||||||
|
|
||||||
println!("Church Core API Client Test");
|
|
||||||
println!("==========================");
|
|
||||||
|
|
||||||
// Test health check
|
|
||||||
match client.health_check().await {
|
|
||||||
Ok(true) => println!("✅ Health check passed"),
|
|
||||||
Ok(false) => println!("❌ Health check failed"),
|
|
||||||
Err(e) => println!("❌ Health check error: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test upcoming events
|
|
||||||
match client.get_upcoming_events(Some(5)).await {
|
|
||||||
Ok(events) => {
|
|
||||||
println!("✅ Retrieved {} upcoming events", events.len());
|
|
||||||
for event in events.iter().take(3) {
|
|
||||||
println!(" - {}: {}", event.title, event.start_time.format("%Y-%m-%d %H:%M"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("❌ Failed to get events: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test current bulletin
|
|
||||||
match client.get_current_bulletin().await {
|
|
||||||
Ok(Some(bulletin)) => {
|
|
||||||
println!("✅ Retrieved current bulletin: {}", bulletin.title);
|
|
||||||
}
|
|
||||||
Ok(None) => println!("ℹ️ No current bulletin found"),
|
|
||||||
Err(e) => println!("❌ Failed to get bulletin: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test configuration
|
|
||||||
match client.get_config().await {
|
|
||||||
Ok(config) => {
|
|
||||||
println!("✅ Retrieved church config");
|
|
||||||
if let Some(name) = &config.church_name {
|
|
||||||
println!(" Church: {}", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("❌ Failed to get config: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test sermons
|
|
||||||
match client.get_recent_sermons(Some(5)).await {
|
|
||||||
Ok(sermons) => {
|
|
||||||
println!("✅ Retrieved {} recent sermons", sermons.len());
|
|
||||||
for sermon in sermons.iter().take(2) {
|
|
||||||
println!(" - {}: {}", sermon.title, sermon.speaker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("❌ Failed to get sermons: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test livestreams
|
|
||||||
match client.get_livestreams().await {
|
|
||||||
Ok(streams) => {
|
|
||||||
println!("✅ Retrieved {} livestream archives", streams.len());
|
|
||||||
for stream in streams.iter().take(2) {
|
|
||||||
println!(" - {}: {}", stream.title, stream.speaker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("❌ Failed to get livestreams: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test cache stats
|
|
||||||
let (cache_size, max_size) = client.get_cache_stats().await;
|
|
||||||
println!("📊 Cache: {}/{} items", cache_size, max_size);
|
|
||||||
|
|
||||||
// Test streaming URL generation
|
|
||||||
println!("\n🎬 Testing Streaming URLs:");
|
|
||||||
let media_id = "test-id-123";
|
|
||||||
let base_url = "https://api.rockvilletollandsda.church";
|
|
||||||
|
|
||||||
let av1_url = DeviceCapabilities::get_streaming_url(base_url, media_id, StreamingCapability::AV1);
|
|
||||||
println!(" AV1: {}", av1_url.url);
|
|
||||||
|
|
||||||
let hls_url = DeviceCapabilities::get_streaming_url(base_url, media_id, StreamingCapability::HLS);
|
|
||||||
println!(" HLS: {}", hls_url.url);
|
|
||||||
|
|
||||||
let optimal_url = DeviceCapabilities::get_optimal_streaming_url(base_url, media_id);
|
|
||||||
println!(" Optimal: {} ({:?})", optimal_url.url, optimal_url.capability);
|
|
||||||
|
|
||||||
println!("\nTest completed!");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
339
church-core/src/cache/mod.rs
vendored
339
church-core/src/cache/mod.rs
vendored
|
@ -1,339 +0,0 @@
|
||||||
use serde::{de::DeserializeOwned, Serialize, Deserialize};
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CachedHttpResponse {
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
pub content_type: String,
|
|
||||||
pub headers: HashMap<String, String>,
|
|
||||||
pub status_code: u16,
|
|
||||||
#[serde(with = "instant_serde")]
|
|
||||||
pub cached_at: Instant,
|
|
||||||
#[serde(with = "instant_serde")]
|
|
||||||
pub expires_at: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom serializer for Instant (can't be serialized directly)
|
|
||||||
mod instant_serde {
|
|
||||||
use super::*;
|
|
||||||
use serde::{Deserializer, Serializer};
|
|
||||||
|
|
||||||
pub fn serialize<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
// Convert to duration since app start - this is approximate but works for our use case
|
|
||||||
let duration_since_start = instant.elapsed();
|
|
||||||
serializer.serialize_u64(duration_since_start.as_secs())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let secs = <u64 as Deserialize>::deserialize(deserializer)?;
|
|
||||||
// For loaded items, set as if they were cached "now" minus the stored duration
|
|
||||||
// This isn't perfect but works for expiration checking
|
|
||||||
Ok(Instant::now() - Duration::from_secs(secs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified cache interface - removed trait object complexity
|
|
||||||
// Each cache type will implement these methods directly
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct CacheEntry {
|
|
||||||
data: Vec<u8>,
|
|
||||||
expires_at: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CacheEntry {
|
|
||||||
fn new(data: Vec<u8>, ttl: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
data,
|
|
||||||
expires_at: Instant::now() + ttl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_expired(&self) -> bool {
|
|
||||||
Instant::now() > self.expires_at
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MemoryCache {
|
|
||||||
store: Arc<RwLock<HashMap<String, CacheEntry>>>,
|
|
||||||
http_store: Arc<RwLock<HashMap<String, CachedHttpResponse>>>,
|
|
||||||
max_size: usize,
|
|
||||||
cache_dir: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryCache {
|
|
||||||
pub fn new(max_size: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
store: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
http_store: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
max_size,
|
|
||||||
cache_dir: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_disk_cache(mut self, cache_dir: PathBuf) -> Self {
|
|
||||||
self.cache_dir = Some(cache_dir);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cache_file_path(&self, url: &str) -> Option<PathBuf> {
|
|
||||||
self.cache_dir.as_ref().map(|dir| {
|
|
||||||
// Create a safe filename from URL
|
|
||||||
let hash = {
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
url.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
};
|
|
||||||
dir.join(format!("cache_{}.json", hash))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cleanup_expired(&self) {
|
|
||||||
let mut store = self.store.write().await;
|
|
||||||
let now = Instant::now();
|
|
||||||
store.retain(|_, entry| entry.expires_at > now);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_capacity(&self) {
|
|
||||||
let mut store = self.store.write().await;
|
|
||||||
|
|
||||||
if store.len() >= self.max_size {
|
|
||||||
// Remove oldest entries if we're at capacity
|
|
||||||
// Collect keys to remove to avoid borrow issues
|
|
||||||
let mut to_remove: Vec<String> = Vec::new();
|
|
||||||
{
|
|
||||||
let entries: Vec<_> = store.iter().collect();
|
|
||||||
let mut sorted_entries = entries;
|
|
||||||
sorted_entries.sort_by_key(|(_, entry)| entry.expires_at);
|
|
||||||
|
|
||||||
let remove_count = sorted_entries.len().saturating_sub(self.max_size / 2);
|
|
||||||
for (key, _) in sorted_entries.into_iter().take(remove_count) {
|
|
||||||
to_remove.push(key.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now remove the keys
|
|
||||||
for key in to_remove {
|
|
||||||
store.remove(&key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryCache {
|
|
||||||
pub async fn get<T>(&self, key: &str) -> Option<T>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + 'static,
|
|
||||||
{
|
|
||||||
// Clean up expired entries periodically
|
|
||||||
if rand::random::<f32>() < 0.1 {
|
|
||||||
self.cleanup_expired().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let store = self.store.read().await;
|
|
||||||
if let Some(entry) = store.get(key) {
|
|
||||||
if !entry.is_expired() {
|
|
||||||
if let Ok(value) = serde_json::from_slice(&entry.data) {
|
|
||||||
return Some(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set<T>(&self, key: &str, value: &T, ttl: Duration)
|
|
||||||
where
|
|
||||||
T: Serialize + Send + Sync,
|
|
||||||
{
|
|
||||||
if let Ok(data) = serde_json::to_vec(value) {
|
|
||||||
self.ensure_capacity().await;
|
|
||||||
|
|
||||||
let mut store = self.store.write().await;
|
|
||||||
store.insert(key.to_string(), CacheEntry::new(data, ttl));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove(&self, key: &str) {
|
|
||||||
let mut store = self.store.write().await;
|
|
||||||
store.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clear(&self) {
|
|
||||||
let mut store = self.store.write().await;
|
|
||||||
store.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn len(&self) -> usize {
|
|
||||||
let store = self.store.read().await;
|
|
||||||
store.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn invalidate_prefix(&self, prefix: &str) {
|
|
||||||
let mut store = self.store.write().await;
|
|
||||||
store.retain(|key, _| !key.starts_with(prefix));
|
|
||||||
|
|
||||||
let mut http_store = self.http_store.write().await;
|
|
||||||
http_store.retain(|key, _| !key.starts_with(prefix));
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP Response Caching Methods
|
|
||||||
|
|
||||||
pub async fn get_http_response(&self, url: &str) -> Option<CachedHttpResponse> {
|
|
||||||
// Clean up expired entries periodically
|
|
||||||
if rand::random::<f32>() < 0.1 {
|
|
||||||
self.cleanup_expired_http().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Check memory cache first (fastest)
|
|
||||||
{
|
|
||||||
let store = self.http_store.read().await;
|
|
||||||
println!("🔍 Memory cache lookup for: {}", url);
|
|
||||||
println!("🔍 Memory cache has {} entries", store.len());
|
|
||||||
|
|
||||||
if let Some(response) = store.get(url) {
|
|
||||||
if !response.is_expired() {
|
|
||||||
println!("🔍 Memory cache HIT - found valid entry");
|
|
||||||
return Some(response.clone());
|
|
||||||
} else {
|
|
||||||
println!("🔍 Memory cache entry expired");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check disk cache (persistent)
|
|
||||||
if let Some(cache_path) = self.get_cache_file_path(url) {
|
|
||||||
println!("🔍 Checking disk cache at: {:?}", cache_path);
|
|
||||||
|
|
||||||
if let Ok(file_content) = fs::read(&cache_path).await {
|
|
||||||
if let Ok(cached_response) = serde_json::from_slice::<CachedHttpResponse>(&file_content) {
|
|
||||||
if !cached_response.is_expired() {
|
|
||||||
println!("🔍 Disk cache HIT - loading into memory");
|
|
||||||
|
|
||||||
// Load back into memory cache for faster future access
|
|
||||||
let mut store = self.http_store.write().await;
|
|
||||||
store.insert(url.to_string(), cached_response.clone());
|
|
||||||
|
|
||||||
return Some(cached_response);
|
|
||||||
} else {
|
|
||||||
println!("🔍 Disk cache entry expired, removing file");
|
|
||||||
let _ = fs::remove_file(&cache_path).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("🔍 Failed to parse disk cache file");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("🔍 No disk cache file found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("🔍 Cache MISS - no valid entry found");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_http_response(&self, url: &str, response: CachedHttpResponse) {
|
|
||||||
self.ensure_http_capacity().await;
|
|
||||||
|
|
||||||
// Store in memory cache
|
|
||||||
let mut store = self.http_store.write().await;
|
|
||||||
println!("🔍 Storing in memory cache: {}", url);
|
|
||||||
println!("🔍 Memory cache will have {} entries after insert", store.len() + 1);
|
|
||||||
store.insert(url.to_string(), response.clone());
|
|
||||||
drop(store); // Release the lock before async disk operation
|
|
||||||
|
|
||||||
// Store in disk cache (async, non-blocking)
|
|
||||||
if let Some(cache_path) = self.get_cache_file_path(url) {
|
|
||||||
println!("🔍 Storing to disk cache: {:?}", cache_path);
|
|
||||||
|
|
||||||
// Ensure cache directory exists
|
|
||||||
if let Some(parent) = cache_path.parent() {
|
|
||||||
let _ = fs::create_dir_all(parent).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize and save to disk
|
|
||||||
match serde_json::to_vec(&response) {
|
|
||||||
Ok(serialized) => {
|
|
||||||
if let Err(e) = fs::write(&cache_path, serialized).await {
|
|
||||||
println!("🔍 Failed to write disk cache: {}", e);
|
|
||||||
} else {
|
|
||||||
println!("🔍 Successfully saved to disk cache");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("🔍 Failed to serialize for disk cache: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cleanup_expired_http(&self) {
|
|
||||||
let mut store = self.http_store.write().await;
|
|
||||||
let now = Instant::now();
|
|
||||||
store.retain(|_, response| response.expires_at > now);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_http_capacity(&self) {
|
|
||||||
let mut store = self.http_store.write().await;
|
|
||||||
|
|
||||||
if store.len() >= self.max_size {
|
|
||||||
// Remove oldest entries if we're at capacity
|
|
||||||
let mut to_remove: Vec<String> = Vec::new();
|
|
||||||
{
|
|
||||||
let entries: Vec<_> = store.iter().collect();
|
|
||||||
let mut sorted_entries = entries;
|
|
||||||
sorted_entries.sort_by_key(|(_, response)| response.cached_at);
|
|
||||||
|
|
||||||
let remove_count = sorted_entries.len().saturating_sub(self.max_size / 2);
|
|
||||||
for (key, _) in sorted_entries.into_iter().take(remove_count) {
|
|
||||||
to_remove.push(key.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now remove the keys
|
|
||||||
for key in to_remove {
|
|
||||||
store.remove(&key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CachedHttpResponse {
|
|
||||||
pub fn new(
|
|
||||||
data: Vec<u8>,
|
|
||||||
content_type: String,
|
|
||||||
headers: HashMap<String, String>,
|
|
||||||
status_code: u16,
|
|
||||||
ttl: Duration
|
|
||||||
) -> Self {
|
|
||||||
let now = Instant::now();
|
|
||||||
Self {
|
|
||||||
data,
|
|
||||||
content_type,
|
|
||||||
headers,
|
|
||||||
status_code,
|
|
||||||
cached_at: now,
|
|
||||||
expires_at: now + ttl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_expired(&self) -> bool {
|
|
||||||
Instant::now() > self.expires_at
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add rand dependency for periodic cleanup
|
|
||||||
// This is a simple implementation - in production you might want to use a more sophisticated cache like moka
|
|
|
@ -1,78 +0,0 @@
|
||||||
namespace church_core {
|
|
||||||
string fetch_events_json();
|
|
||||||
string fetch_bulletins_json();
|
|
||||||
string fetch_sermons_json();
|
|
||||||
string fetch_bible_verse_json(string query);
|
|
||||||
string fetch_random_bible_verse_json();
|
|
||||||
string fetch_scripture_verses_for_sermon_json(string sermon_id);
|
|
||||||
string fetch_config_json();
|
|
||||||
string fetch_current_bulletin_json();
|
|
||||||
string fetch_featured_events_json();
|
|
||||||
string fetch_stream_status_json();
|
|
||||||
boolean get_stream_live_status();
|
|
||||||
string get_livestream_url();
|
|
||||||
string fetch_live_stream_json();
|
|
||||||
string fetch_livestream_archive_json();
|
|
||||||
string submit_contact_json(string name, string email, string message);
|
|
||||||
string submit_contact_v2_json(string name, string email, string subject, string message, string phone);
|
|
||||||
string submit_contact_v2_json_legacy(string first_name, string last_name, string email, string subject, string message);
|
|
||||||
string fetch_cached_image_base64(string url);
|
|
||||||
string get_optimal_streaming_url(string media_id);
|
|
||||||
boolean device_supports_av1();
|
|
||||||
string get_av1_streaming_url(string media_id);
|
|
||||||
string get_hls_streaming_url(string media_id);
|
|
||||||
|
|
||||||
// Scripture formatting utilities
|
|
||||||
string format_scripture_text_json(string scripture_text);
|
|
||||||
string extract_scripture_references_string(string scripture_text);
|
|
||||||
string create_sermon_share_items_json(string title, string speaker, string? video_url, string? audio_url);
|
|
||||||
|
|
||||||
// Form validation functions
|
|
||||||
string validate_contact_form_json(string form_json);
|
|
||||||
boolean validate_email_address(string email);
|
|
||||||
boolean validate_phone_number(string phone);
|
|
||||||
|
|
||||||
// Event formatting functions
|
|
||||||
string format_event_for_display_json(string event_json);
|
|
||||||
string format_time_range_string(string start_time, string end_time);
|
|
||||||
boolean is_multi_day_event_check(string date);
|
|
||||||
|
|
||||||
// Home feed aggregation
|
|
||||||
string generate_home_feed_json(string events_json, string sermons_json, string bulletins_json, string verse_json);
|
|
||||||
|
|
||||||
// Media type management
|
|
||||||
string get_media_type_display_name(string media_type_str);
|
|
||||||
string get_media_type_icon(string media_type_str);
|
|
||||||
string filter_sermons_by_media_type(string sermons_json, string media_type_str);
|
|
||||||
|
|
||||||
// Individual config getter functions (RTSDA architecture compliant)
|
|
||||||
string get_church_name();
|
|
||||||
string get_contact_phone();
|
|
||||||
string get_contact_email();
|
|
||||||
string get_brand_color();
|
|
||||||
string get_about_text();
|
|
||||||
string get_donation_url();
|
|
||||||
string get_church_address();
|
|
||||||
string get_church_physical_address();
|
|
||||||
string get_church_po_box();
|
|
||||||
sequence<f64> get_coordinates();
|
|
||||||
string get_website_url();
|
|
||||||
string get_facebook_url();
|
|
||||||
string get_youtube_url();
|
|
||||||
string get_instagram_url();
|
|
||||||
string get_mission_statement();
|
|
||||||
|
|
||||||
// Calendar event parsing (RTSDA architecture compliant)
|
|
||||||
string create_calendar_event_data(string event_json);
|
|
||||||
|
|
||||||
// JSON parsing functions (RTSDA architecture compliance)
|
|
||||||
string parse_events_from_json(string events_json);
|
|
||||||
string parse_sermons_from_json(string sermons_json);
|
|
||||||
string parse_bulletins_from_json(string bulletins_json);
|
|
||||||
string parse_bible_verse_from_json(string verse_json);
|
|
||||||
string parse_contact_result_from_json(string result_json);
|
|
||||||
string generate_verse_description(string verses_json);
|
|
||||||
string extract_full_verse_text(string verses_json);
|
|
||||||
string extract_stream_url_from_status(string status_json);
|
|
||||||
string parse_calendar_event_data(string calendar_json);
|
|
||||||
};
|
|
|
@ -1,107 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{
|
|
||||||
NewBulletin, BulletinUpdate,
|
|
||||||
NewEvent, EventUpdate, PendingEvent,
|
|
||||||
User, Schedule, NewSchedule, ScheduleUpdate,
|
|
||||||
ApiVersion,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Admin Bulletin Management
|
|
||||||
pub async fn create_bulletin(client: &ChurchApiClient, bulletin: NewBulletin) -> Result<String> {
|
|
||||||
client.post_api_with_version("/admin/bulletins", &bulletin, ApiVersion::V1).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_bulletin(client: &ChurchApiClient, id: &str, update: BulletinUpdate) -> Result<()> {
|
|
||||||
let path = format!("/admin/bulletins/{}", id);
|
|
||||||
client.put_api(&path, &update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_bulletin(client: &ChurchApiClient, id: &str) -> Result<()> {
|
|
||||||
let path = format!("/admin/bulletins/{}", id);
|
|
||||||
client.delete_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Event Management
|
|
||||||
// Note: Event creation must go through public submission form
|
|
||||||
|
|
||||||
pub async fn update_admin_event(client: &ChurchApiClient, id: &str, update: EventUpdate) -> Result<()> {
|
|
||||||
let path = format!("/admin/events/{}", id);
|
|
||||||
client.put_api(&path, &update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_admin_event(client: &ChurchApiClient, id: &str) -> Result<()> {
|
|
||||||
let path = format!("/admin/events/{}", id);
|
|
||||||
client.delete_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Pending Events Management
|
|
||||||
pub async fn get_pending_events(client: &ChurchApiClient) -> Result<Vec<PendingEvent>> {
|
|
||||||
client.get_api("/admin/events/pending").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn approve_pending_event(client: &ChurchApiClient, id: &str) -> Result<()> {
|
|
||||||
let path = format!("/admin/events/pending/{}/approve", id);
|
|
||||||
let response: crate::models::ApiResponse<()> = client.post(&path, &()).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Failed to approve pending event".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reject_pending_event(client: &ChurchApiClient, id: &str) -> Result<()> {
|
|
||||||
let path = format!("/admin/events/pending/{}/reject", id);
|
|
||||||
let response: crate::models::ApiResponse<()> = client.post(&path, &()).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Failed to reject pending event".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_pending_event(client: &ChurchApiClient, id: &str) -> Result<()> {
|
|
||||||
let path = format!("/admin/events/pending/{}", id);
|
|
||||||
client.delete_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin User Management
|
|
||||||
pub async fn get_users(client: &ChurchApiClient) -> Result<Vec<User>> {
|
|
||||||
client.get_api("/admin/users").await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Schedule Management
|
|
||||||
pub async fn create_schedule(client: &ChurchApiClient, schedule: NewSchedule) -> Result<String> {
|
|
||||||
client.post_api("/admin/schedule", &schedule).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_schedule(client: &ChurchApiClient, date: &str, update: ScheduleUpdate) -> Result<()> {
|
|
||||||
let path = format!("/admin/schedule/{}", date);
|
|
||||||
client.put_api(&path, &update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_schedule(client: &ChurchApiClient, date: &str) -> Result<()> {
|
|
||||||
let path = format!("/admin/schedule/{}", date);
|
|
||||||
client.delete_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_schedules(client: &ChurchApiClient) -> Result<Vec<Schedule>> {
|
|
||||||
client.get_api("/admin/schedule").await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Config Management
|
|
||||||
pub async fn get_admin_config(client: &ChurchApiClient) -> Result<crate::models::ChurchConfig> {
|
|
||||||
client.get_api("/admin/config").await
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{BibleVerse, VerseOfTheDay, VerseCategory, PaginationParams, ApiListResponse, ApiVersion},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_random_verse(client: &ChurchApiClient) -> Result<BibleVerse> {
|
|
||||||
// The response format is {success: bool, data: Verse}
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct VerseResponse {
|
|
||||||
success: bool,
|
|
||||||
data: ApiVerse,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct ApiVerse {
|
|
||||||
id: String,
|
|
||||||
reference: String,
|
|
||||||
text: String,
|
|
||||||
#[serde(rename = "is_active")]
|
|
||||||
is_active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = client.build_url("/bible_verses/random");
|
|
||||||
let raw_response = client.client.get(&url).send().await?;
|
|
||||||
let response_text = raw_response.text().await?;
|
|
||||||
let response: VerseResponse = serde_json::from_str(&response_text)
|
|
||||||
.map_err(|e| crate::error::ChurchApiError::Json(e))?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(BibleVerse::new(response.data.text, response.data.reference))
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api("Bible verse API returned success=false".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_verse_of_the_day(client: &ChurchApiClient) -> Result<VerseOfTheDay> {
|
|
||||||
client.get_api("/bible/verse-of-the-day").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_verse_by_reference(client: &ChurchApiClient, reference: &str) -> Result<Option<BibleVerse>> {
|
|
||||||
let path = format!("/bible/verse?reference={}", urlencoding::encode(reference));
|
|
||||||
|
|
||||||
match client.get_api(&path).await {
|
|
||||||
Ok(verse) => Ok(Some(verse)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_verses_by_category(client: &ChurchApiClient, category: VerseCategory, limit: Option<u32>) -> Result<Vec<BibleVerse>> {
|
|
||||||
let mut path = format!("/bible/category/{}", category.display_name().to_lowercase());
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: crate::models::ApiListResponse<BibleVerse> = client.get_api_list(&path).await?;
|
|
||||||
Ok(response.data.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_verses(client: &ChurchApiClient, query: &str, limit: Option<u32>) -> Result<Vec<BibleVerse>> {
|
|
||||||
let mut path = format!("/bible_verses/search?q={}", urlencoding::encode(query));
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("&limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bible_verses/search endpoint returns a custom format with additional fields
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct ApiBibleVerse {
|
|
||||||
id: String,
|
|
||||||
reference: String,
|
|
||||||
text: String,
|
|
||||||
is_active: bool,
|
|
||||||
created_at: String,
|
|
||||||
updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct BibleSearchResponse {
|
|
||||||
success: bool,
|
|
||||||
data: Vec<ApiBibleVerse>,
|
|
||||||
message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = client.build_url(&path);
|
|
||||||
let raw_response = client.client.get(&url).send().await?;
|
|
||||||
let response_text = raw_response.text().await?;
|
|
||||||
let response: BibleSearchResponse = serde_json::from_str(&response_text)
|
|
||||||
.map_err(|e| crate::error::ChurchApiError::Json(e))?;
|
|
||||||
if response.success {
|
|
||||||
// Convert ApiBibleVerse to BibleVerse
|
|
||||||
let verses = response.data.into_iter()
|
|
||||||
.map(|api_verse| BibleVerse::new(api_verse.text, api_verse.reference))
|
|
||||||
.collect();
|
|
||||||
Ok(verses)
|
|
||||||
} else {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
pub async fn get_random_verse_v2(client: &ChurchApiClient) -> Result<BibleVerse> {
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct VerseResponse {
|
|
||||||
success: bool,
|
|
||||||
data: ApiVerse,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct ApiVerse {
|
|
||||||
id: String,
|
|
||||||
reference: String,
|
|
||||||
text: String,
|
|
||||||
#[serde(rename = "is_active")]
|
|
||||||
is_active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = client.build_url_with_version("/bible_verses/random", ApiVersion::V2);
|
|
||||||
let raw_response = client.client.get(&url).send().await?;
|
|
||||||
let response_text = raw_response.text().await?;
|
|
||||||
let response: VerseResponse = serde_json::from_str(&response_text)
|
|
||||||
.map_err(|e| crate::error::ChurchApiError::Json(e))?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(BibleVerse::new(response.data.text, response.data.reference))
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api("Bible verse API returned success=false".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_bible_verses_v2(client: &ChurchApiClient, params: Option<PaginationParams>) -> Result<ApiListResponse<BibleVerse>> {
|
|
||||||
let mut path = "/bible_verses".to_string();
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_list_with_version(&path, ApiVersion::V2).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_verses_v2(client: &ChurchApiClient, query: &str, limit: Option<u32>) -> Result<Vec<BibleVerse>> {
|
|
||||||
let mut path = format!("/bible_verses/search?q={}", urlencoding::encode(query));
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("&limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: crate::models::ApiListResponse<BibleVerse> = client.get_api_list_with_version(&path, ApiVersion::V2).await?;
|
|
||||||
Ok(response.data.items)
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{Bulletin, NewBulletin, PaginationParams, ApiListResponse, ApiVersion},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_bulletins(client: &ChurchApiClient, active_only: bool) -> Result<Vec<Bulletin>> {
|
|
||||||
let path = if active_only {
|
|
||||||
"/bulletins?active=true"
|
|
||||||
} else {
|
|
||||||
"/bulletins"
|
|
||||||
};
|
|
||||||
|
|
||||||
let response: ApiListResponse<Bulletin> = client.get_api_list(path).await?;
|
|
||||||
Ok(response.data.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_current_bulletin(client: &ChurchApiClient) -> Result<Option<Bulletin>> {
|
|
||||||
match client.get_api("/bulletins/current").await {
|
|
||||||
Ok(bulletin) => Ok(Some(bulletin)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_bulletin(client: &ChurchApiClient, id: &str) -> Result<Option<Bulletin>> {
|
|
||||||
let path = format!("/bulletins/{}", id);
|
|
||||||
|
|
||||||
match client.get_api(&path).await {
|
|
||||||
Ok(bulletin) => Ok(Some(bulletin)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_bulletin(client: &ChurchApiClient, bulletin: NewBulletin) -> Result<String> {
|
|
||||||
client.post_api("/bulletins", &bulletin).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_next_bulletin(client: &ChurchApiClient) -> Result<Option<Bulletin>> {
|
|
||||||
match client.get_api("/bulletins/next").await {
|
|
||||||
Ok(bulletin) => Ok(Some(bulletin)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
pub async fn get_bulletins_v2(client: &ChurchApiClient, params: Option<PaginationParams>) -> Result<ApiListResponse<Bulletin>> {
|
|
||||||
let mut path = "/bulletins".to_string();
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = client.build_url_with_version(&path, ApiVersion::V2);
|
|
||||||
let response: ApiListResponse<Bulletin> = client.get(&url).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(response)
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Unknown API error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_current_bulletin_v2(client: &ChurchApiClient) -> Result<Option<Bulletin>> {
|
|
||||||
match client.get_api_with_version("/bulletins/current", ApiVersion::V2).await {
|
|
||||||
Ok(bulletin) => Ok(Some(bulletin)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_next_bulletin_v2(client: &ChurchApiClient) -> Result<Option<Bulletin>> {
|
|
||||||
match client.get_api_with_version("/bulletins/next", ApiVersion::V2).await {
|
|
||||||
Ok(bulletin) => Ok(Some(bulletin)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{ChurchConfig, Schedule, ConferenceData, ApiVersion},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_config(client: &ChurchApiClient) -> Result<ChurchConfig> {
|
|
||||||
client.get("/config").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_config_by_id(client: &ChurchApiClient, record_id: &str) -> Result<ChurchConfig> {
|
|
||||||
let path = format!("/config/records/{}", record_id);
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_config(client: &ChurchApiClient, config: ChurchConfig) -> Result<()> {
|
|
||||||
client.put_api("/config", &config).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
pub async fn get_config_v2(client: &ChurchApiClient) -> Result<ChurchConfig> {
|
|
||||||
client.get_api_with_version("/config", ApiVersion::V2).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule endpoints
|
|
||||||
pub async fn get_schedule(client: &ChurchApiClient, date: Option<&str>) -> Result<Schedule> {
|
|
||||||
let path = if let Some(date) = date {
|
|
||||||
format!("/schedule?date={}", date)
|
|
||||||
} else {
|
|
||||||
"/schedule".to_string()
|
|
||||||
};
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_schedule_v2(client: &ChurchApiClient, date: Option<&str>) -> Result<Schedule> {
|
|
||||||
let path = if let Some(date) = date {
|
|
||||||
format!("/schedule?date={}", date)
|
|
||||||
} else {
|
|
||||||
"/schedule".to_string()
|
|
||||||
};
|
|
||||||
client.get_api_with_version(&path, ApiVersion::V2).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_conference_data(client: &ChurchApiClient) -> Result<ConferenceData> {
|
|
||||||
client.get_api("/conference-data").await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_conference_data_v2(client: &ChurchApiClient) -> Result<ConferenceData> {
|
|
||||||
client.get_api_with_version("/conference-data", ApiVersion::V2).await
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{ContactForm, ContactSubmission, ContactStatus, PaginationParams, ApiListResponse, ApiVersion},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn submit_contact_form(client: &ChurchApiClient, form: ContactForm) -> Result<String> {
|
|
||||||
// Create payload matching the expected format from iOS app
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"first_name": form.name.split_whitespace().next().unwrap_or(&form.name),
|
|
||||||
"last_name": form.name.split_whitespace().nth(1).unwrap_or(""),
|
|
||||||
"email": form.email,
|
|
||||||
"phone": form.phone.unwrap_or_default(),
|
|
||||||
"message": form.message
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use the main API subdomain for consistency
|
|
||||||
let contact_url = client.build_url("/contact");
|
|
||||||
|
|
||||||
let response = client.client
|
|
||||||
.post(contact_url)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&payload)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok("Contact form submitted successfully".to_string())
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(format!("Contact form submission failed with status: {}", response.status())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_contact_submissions(client: &ChurchApiClient, params: Option<PaginationParams>) -> Result<ApiListResponse<ContactSubmission>> {
|
|
||||||
let mut path = "/contact/submissions".to_string();
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(sort) = ¶ms.sort {
|
|
||||||
query_params.push(("sort", sort.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(filter) = ¶ms.filter {
|
|
||||||
query_params.push(("filter", filter.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_list(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_contact_submission(client: &ChurchApiClient, id: &str) -> Result<Option<ContactSubmission>> {
|
|
||||||
let path = format!("/contact/submissions/{}", id);
|
|
||||||
|
|
||||||
match client.get_api(&path).await {
|
|
||||||
Ok(submission) => Ok(Some(submission)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_contact_submission(
|
|
||||||
client: &ChurchApiClient,
|
|
||||||
id: &str,
|
|
||||||
status: ContactStatus,
|
|
||||||
response: Option<String>
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = format!("/contact/submissions/{}", id);
|
|
||||||
|
|
||||||
let update_data = serde_json::json!({
|
|
||||||
"status": status,
|
|
||||||
"response": response
|
|
||||||
});
|
|
||||||
|
|
||||||
client.put_api(&path, &update_data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
pub async fn submit_contact_form_v2(client: &ChurchApiClient, form: ContactForm) -> Result<String> {
|
|
||||||
let mut payload = serde_json::json!({
|
|
||||||
"name": form.name,
|
|
||||||
"email": form.email,
|
|
||||||
"subject": form.subject,
|
|
||||||
"message": form.message
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add phone field if provided
|
|
||||||
if let Some(phone) = &form.phone {
|
|
||||||
if !phone.trim().is_empty() {
|
|
||||||
payload["phone"] = serde_json::json!(phone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = client.build_url_with_version("/contact", ApiVersion::V2);
|
|
||||||
|
|
||||||
let response = client.client
|
|
||||||
.post(url)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&payload)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok("Contact form submitted successfully".to_string())
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(format!("Contact form submission failed with status: {}", response.status())))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{Event, NewEvent, EventUpdate, EventSubmission, PaginationParams, ApiListResponse, ApiVersion},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_events(client: &ChurchApiClient, params: Option<PaginationParams>) -> Result<ApiListResponse<Event>> {
|
|
||||||
let mut path = "/events".to_string();
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(sort) = ¶ms.sort {
|
|
||||||
query_params.push(("sort", sort.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(filter) = ¶ms.filter {
|
|
||||||
query_params.push(("filter", filter.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_list(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_upcoming_events(client: &ChurchApiClient, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
let mut path = "/events/upcoming".to_string();
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_event(client: &ChurchApiClient, id: &str) -> Result<Option<Event>> {
|
|
||||||
let path = format!("/events/{}", id);
|
|
||||||
|
|
||||||
match client.get_api(&path).await {
|
|
||||||
Ok(event) => Ok(Some(event)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_event(client: &ChurchApiClient, event: NewEvent) -> Result<String> {
|
|
||||||
client.post_api("/events", &event).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_event(client: &ChurchApiClient, id: &str, update: EventUpdate) -> Result<()> {
|
|
||||||
let path = format!("/events/{}", id);
|
|
||||||
client.put_api(&path, &update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_event(client: &ChurchApiClient, id: &str) -> Result<()> {
|
|
||||||
let path = format!("/events/{}", id);
|
|
||||||
client.delete_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_featured_events(client: &ChurchApiClient, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
let mut path = "/events/featured".to_string();
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_events_by_category(client: &ChurchApiClient, category: &str, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
let mut path = format!("/events/category/{}", category);
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_events_by_date_range(
|
|
||||||
client: &ChurchApiClient,
|
|
||||||
start_date: &str,
|
|
||||||
end_date: &str
|
|
||||||
) -> Result<Vec<Event>> {
|
|
||||||
let path = format!("/events/range?start={}&end={}",
|
|
||||||
urlencoding::encode(start_date),
|
|
||||||
urlencoding::encode(end_date)
|
|
||||||
);
|
|
||||||
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_events(client: &ChurchApiClient, query: &str, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
let mut path = format!("/events/search?q={}", urlencoding::encode(query));
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("&limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upload_event_image(client: &ChurchApiClient, event_id: &str, image_data: Vec<u8>, filename: String) -> Result<String> {
|
|
||||||
let path = format!("/events/{}/image", event_id);
|
|
||||||
client.upload_file(&path, image_data, filename, "image".to_string()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
pub async fn get_events_v2(client: &ChurchApiClient, params: Option<PaginationParams>) -> Result<ApiListResponse<Event>> {
|
|
||||||
let mut path = "/events".to_string();
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(sort) = ¶ms.sort {
|
|
||||||
query_params.push(("sort", sort.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(filter) = ¶ms.filter {
|
|
||||||
query_params.push(("filter", filter.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_list_with_version(&path, ApiVersion::V2).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_upcoming_events_v2(client: &ChurchApiClient, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
let mut path = "/events/upcoming".to_string();
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_with_version(&path, ApiVersion::V2).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_featured_events_v2(client: &ChurchApiClient, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
let mut path = "/events/featured".to_string();
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_with_version(&path, ApiVersion::V2).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_event_v2(client: &ChurchApiClient, id: &str) -> Result<Option<Event>> {
|
|
||||||
let path = format!("/events/{}", id);
|
|
||||||
|
|
||||||
match client.get_api_with_version(&path, ApiVersion::V2).await {
|
|
||||||
Ok(event) => Ok(Some(event)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn submit_event(client: &ChurchApiClient, submission: EventSubmission) -> Result<String> {
|
|
||||||
client.post_api("/events/submit", &submission).await
|
|
||||||
}
|
|
|
@ -1,402 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::{ChurchApiError, Result},
|
|
||||||
models::{ApiResponse, ApiListResponse, ApiVersion},
|
|
||||||
cache::CachedHttpResponse,
|
|
||||||
};
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
use std::{collections::HashMap, time::Duration};
|
|
||||||
|
|
||||||
impl ChurchApiClient {
|
|
||||||
pub(crate) async fn get<T>(&self, path: &str) -> Result<T>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + Sync + serde::Serialize + 'static,
|
|
||||||
{
|
|
||||||
self.get_with_version(path, ApiVersion::V1).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_with_version<T>(&self, path: &str, version: ApiVersion) -> Result<T>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + Sync + serde::Serialize + 'static,
|
|
||||||
{
|
|
||||||
let cache_key = format!("GET:{}:{:?}", path, version);
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
if self.config.enable_offline_mode {
|
|
||||||
if let Some(cached) = self.cache.get::<T>(&cache_key).await {
|
|
||||||
return Ok(cached);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = self.build_url_with_version(path, version);
|
|
||||||
let request = self.client.get(&url);
|
|
||||||
let request = self.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
let error_text = response.text().await?;
|
|
||||||
return Err(crate::error::ChurchApiError::Api(format!("HTTP {}: {}", status, error_text)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response_text = response.text().await?;
|
|
||||||
|
|
||||||
let data: T = serde_json::from_str(&response_text).map_err(|e| {
|
|
||||||
crate::error::ChurchApiError::Json(e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
if self.config.enable_offline_mode {
|
|
||||||
self.cache.set(&cache_key, &data, self.config.cache_ttl).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_api<T>(&self, path: &str) -> Result<T>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + Sync + serde::Serialize + 'static,
|
|
||||||
{
|
|
||||||
self.get_api_with_version(path, ApiVersion::V1).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_api_with_version<T>(&self, path: &str, version: ApiVersion) -> Result<T>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + Sync + serde::Serialize + 'static,
|
|
||||||
{
|
|
||||||
let response: ApiResponse<T> = self.get_with_version(path, version).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
response.data.ok_or_else(|| {
|
|
||||||
ChurchApiError::Api("API returned success but no data".to_string())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Unknown API error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_api_list<T>(&self, path: &str) -> Result<ApiListResponse<T>>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + Sync + serde::Serialize + 'static,
|
|
||||||
{
|
|
||||||
self.get_api_list_with_version(path, ApiVersion::V1).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_api_list_with_version<T>(&self, path: &str, version: ApiVersion) -> Result<ApiListResponse<T>>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned + Send + Sync + serde::Serialize + 'static,
|
|
||||||
{
|
|
||||||
let response: ApiListResponse<T> = self.get_with_version(path, version).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(response)
|
|
||||||
} else {
|
|
||||||
Err(ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Unknown API error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn post<T, R>(&self, path: &str, data: &T) -> Result<R>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
R: DeserializeOwned,
|
|
||||||
{
|
|
||||||
self.post_with_version(path, data, ApiVersion::V1).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn post_with_version<T, R>(&self, path: &str, data: &T, version: ApiVersion) -> Result<R>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
R: DeserializeOwned,
|
|
||||||
{
|
|
||||||
let url = self.build_url_with_version(path, version);
|
|
||||||
let request = self.client.post(&url).json(data);
|
|
||||||
let request = self.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
let result: R = response.json().await?;
|
|
||||||
|
|
||||||
// Invalidate related cache entries
|
|
||||||
self.invalidate_cache_prefix(&format!("GET:{}", path.split('?').next().unwrap_or(path))).await;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn post_api<T, R>(&self, path: &str, data: &T) -> Result<R>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
R: DeserializeOwned,
|
|
||||||
{
|
|
||||||
self.post_api_with_version(path, data, ApiVersion::V1).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn post_api_with_version<T, R>(&self, path: &str, data: &T, version: ApiVersion) -> Result<R>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
R: DeserializeOwned,
|
|
||||||
{
|
|
||||||
let response: ApiResponse<R> = self.post_with_version(path, data, version).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
response.data.ok_or_else(|| {
|
|
||||||
ChurchApiError::Api("API returned success but no data".to_string())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Unknown API error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn put<T, R>(&self, path: &str, data: &T) -> Result<R>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
R: DeserializeOwned,
|
|
||||||
{
|
|
||||||
let url = self.build_url(path);
|
|
||||||
let request = self.client.put(&url).json(data);
|
|
||||||
let request = self.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
let result: R = response.json().await?;
|
|
||||||
|
|
||||||
// Invalidate related cache entries
|
|
||||||
self.invalidate_cache_prefix(&format!("GET:{}", path.split('?').next().unwrap_or(path))).await;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn put_api<T>(&self, path: &str, data: &T) -> Result<()>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
let response: ApiResponse<()> = self.put(path, data).await?;
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Unknown API error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn delete(&self, path: &str) -> Result<()> {
|
|
||||||
let url = self.build_url(path);
|
|
||||||
let request = self.client.delete(&url);
|
|
||||||
let request = self.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(ChurchApiError::Http(
|
|
||||||
reqwest::Error::from(response.error_for_status().unwrap_err())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate related cache entries
|
|
||||||
self.invalidate_cache_prefix(&format!("GET:{}", path.split('?').next().unwrap_or(path))).await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn delete_api(&self, path: &str) -> Result<()> {
|
|
||||||
let response: ApiResponse<()> = {
|
|
||||||
let url = self.build_url(path);
|
|
||||||
let request = self.client.delete(&url);
|
|
||||||
let request = self.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
response.json().await?
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.success {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ChurchApiError::Api(
|
|
||||||
response.error
|
|
||||||
.or(response.message)
|
|
||||||
.unwrap_or_else(|| "Unknown API error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_with_retry(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
|
|
||||||
let mut attempts = 0;
|
|
||||||
let max_attempts = self.config.retry_attempts;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
attempts += 1;
|
|
||||||
|
|
||||||
// Clone the request for potential retry
|
|
||||||
let cloned_request = request.try_clone()
|
|
||||||
.ok_or_else(|| ChurchApiError::Internal("Failed to clone request".to_string()))?;
|
|
||||||
|
|
||||||
match cloned_request.send().await {
|
|
||||||
Ok(response) => {
|
|
||||||
let status = response.status();
|
|
||||||
|
|
||||||
if status.is_success() {
|
|
||||||
return Ok(response);
|
|
||||||
} else if status == reqwest::StatusCode::UNAUTHORIZED {
|
|
||||||
return Err(ChurchApiError::Auth("Unauthorized".to_string()));
|
|
||||||
} else if status == reqwest::StatusCode::FORBIDDEN {
|
|
||||||
return Err(ChurchApiError::PermissionDenied);
|
|
||||||
} else if status == reqwest::StatusCode::NOT_FOUND {
|
|
||||||
return Err(ChurchApiError::NotFound);
|
|
||||||
} else if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
|
|
||||||
if attempts < max_attempts {
|
|
||||||
// Exponential backoff for rate limiting
|
|
||||||
let delay = std::time::Duration::from_millis(100 * 2_u64.pow(attempts - 1));
|
|
||||||
tokio::time::sleep(delay).await;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return Err(ChurchApiError::RateLimit);
|
|
||||||
}
|
|
||||||
} else if status.is_server_error() && attempts < max_attempts {
|
|
||||||
// Retry on server errors
|
|
||||||
let delay = std::time::Duration::from_millis(500 * attempts as u64);
|
|
||||||
tokio::time::sleep(delay).await;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return Err(ChurchApiError::Http(
|
|
||||||
reqwest::Error::from(response.error_for_status().unwrap_err())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if attempts < max_attempts && (e.is_timeout() || e.is_connect()) {
|
|
||||||
// Retry on timeout and connection errors
|
|
||||||
let delay = std::time::Duration::from_millis(500 * attempts as u64);
|
|
||||||
tokio::time::sleep(delay).await;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
return Err(ChurchApiError::Http(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn invalidate_cache_prefix(&self, prefix: &str) {
|
|
||||||
self.cache.invalidate_prefix(prefix).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_query_string(&self, params: &[(&str, &str)]) -> String {
|
|
||||||
if params.is_empty() {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let query: Vec<String> = params
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| format!("{}={}", urlencoding::encode(key), urlencoding::encode(value)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
format!("?{}", query.join("&"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn upload_file(&self, path: &str, file_data: Vec<u8>, filename: String, field_name: String) -> Result<String> {
|
|
||||||
let url = self.build_url(path);
|
|
||||||
|
|
||||||
let part = reqwest::multipart::Part::bytes(file_data)
|
|
||||||
.file_name(filename)
|
|
||||||
.mime_str("application/octet-stream")
|
|
||||||
.map_err(|e| ChurchApiError::Internal(format!("Failed to create multipart: {}", e)))?;
|
|
||||||
|
|
||||||
let form = reqwest::multipart::Form::new()
|
|
||||||
.part(field_name, part);
|
|
||||||
|
|
||||||
let request = self.client.post(&url).multipart(form);
|
|
||||||
let request = self.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
let result: ApiResponse<String> = response.json().await?;
|
|
||||||
|
|
||||||
if result.success {
|
|
||||||
result.data.ok_or_else(|| {
|
|
||||||
ChurchApiError::Api("File upload succeeded but no URL returned".to_string())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(ChurchApiError::Api(
|
|
||||||
result.error
|
|
||||||
.or(result.message)
|
|
||||||
.unwrap_or_else(|| "File upload failed".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch an image with HTTP caching support
|
|
||||||
pub async fn get_cached_image(&self, url: &str) -> Result<CachedHttpResponse> {
|
|
||||||
// Check cache first
|
|
||||||
if let Some(cached) = self.cache.get_http_response(url).await {
|
|
||||||
println!("📸 Cache HIT for image: {}", url);
|
|
||||||
return Ok(cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("📸 Cache MISS for image: {}", url);
|
|
||||||
|
|
||||||
// Make HTTP request
|
|
||||||
let request = self.client.get(url);
|
|
||||||
let response = self.send_with_retry(request).await?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
let headers = response.headers().clone();
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
return Err(ChurchApiError::Http(
|
|
||||||
reqwest::Error::from(response.error_for_status().unwrap_err())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract headers we care about
|
|
||||||
let mut header_map = HashMap::new();
|
|
||||||
for (name, value) in headers.iter() {
|
|
||||||
if let Ok(value_str) = value.to_str() {
|
|
||||||
header_map.insert(name.to_string(), value_str.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let content_type = headers
|
|
||||||
.get("content-type")
|
|
||||||
.and_then(|v| v.to_str().ok())
|
|
||||||
.unwrap_or("application/octet-stream")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Get response body
|
|
||||||
let data = response.bytes().await?.to_vec();
|
|
||||||
|
|
||||||
// Determine cache TTL based on content type
|
|
||||||
let ttl = if content_type.starts_with("image/") {
|
|
||||||
Duration::from_secs(24 * 60 * 60) // 24 hours for images
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(5 * 60) // 5 minutes for other content
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create cached response
|
|
||||||
let cached_response = CachedHttpResponse::new(
|
|
||||||
data,
|
|
||||||
content_type,
|
|
||||||
header_map,
|
|
||||||
status.as_u16(),
|
|
||||||
ttl,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store in cache
|
|
||||||
self.cache.set_http_response(url, cached_response.clone()).await;
|
|
||||||
|
|
||||||
Ok(cached_response)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct StreamStatus {
|
|
||||||
pub is_live: bool,
|
|
||||||
pub last_connect_time: Option<DateTime<Utc>>,
|
|
||||||
pub last_disconnect_time: Option<DateTime<Utc>>,
|
|
||||||
pub stream_title: Option<String>,
|
|
||||||
pub stream_url: Option<String>,
|
|
||||||
pub viewer_count: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LiveStream {
|
|
||||||
pub last_connect_time: Option<DateTime<Utc>>,
|
|
||||||
pub last_disconnect_time: Option<DateTime<Utc>>,
|
|
||||||
pub viewer_count: Option<u32>,
|
|
||||||
pub stream_title: Option<String>,
|
|
||||||
pub is_live: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current stream status from Owncast
|
|
||||||
pub async fn get_stream_status(client: &ChurchApiClient) -> Result<StreamStatus> {
|
|
||||||
client.get("/stream/status").await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get live stream info from Owncast
|
|
||||||
pub async fn get_live_stream(client: &ChurchApiClient) -> Result<LiveStream> {
|
|
||||||
client.get("/stream/live").await
|
|
||||||
}
|
|
|
@ -1,446 +0,0 @@
|
||||||
pub mod http;
|
|
||||||
pub mod events;
|
|
||||||
pub mod bulletins;
|
|
||||||
pub mod config;
|
|
||||||
pub mod contact;
|
|
||||||
pub mod sermons;
|
|
||||||
pub mod bible;
|
|
||||||
pub mod admin;
|
|
||||||
pub mod uploads;
|
|
||||||
pub mod livestream;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
cache::MemoryCache,
|
|
||||||
config::ChurchCoreConfig,
|
|
||||||
error::Result,
|
|
||||||
models::*,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
pub struct ChurchApiClient {
|
|
||||||
pub(crate) client: reqwest::Client,
|
|
||||||
pub(crate) config: ChurchCoreConfig,
|
|
||||||
pub(crate) auth_token: Arc<RwLock<Option<AuthToken>>>,
|
|
||||||
pub(crate) cache: Arc<MemoryCache>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChurchApiClient {
|
|
||||||
pub fn new(config: ChurchCoreConfig) -> Result<Self> {
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(config.timeout)
|
|
||||||
.connect_timeout(config.connect_timeout)
|
|
||||||
.pool_idle_timeout(std::time::Duration::from_secs(90))
|
|
||||||
.user_agent(&config.user_agent)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let cache = Arc::new(MemoryCache::new(config.max_cache_size));
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
client,
|
|
||||||
config,
|
|
||||||
auth_token: Arc::new(RwLock::new(None)),
|
|
||||||
cache,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_cache(mut self, cache: Arc<MemoryCache>) -> Self {
|
|
||||||
self.cache = cache;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_auth_token(&self, token: AuthToken) {
|
|
||||||
let mut auth = self.auth_token.write().await;
|
|
||||||
*auth = Some(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clear_auth_token(&self) {
|
|
||||||
let mut auth = self.auth_token.write().await;
|
|
||||||
*auth = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_auth_token(&self) -> Option<AuthToken> {
|
|
||||||
let auth = self.auth_token.read().await;
|
|
||||||
auth.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_authenticated(&self) -> bool {
|
|
||||||
if let Some(token) = self.get_auth_token().await {
|
|
||||||
token.is_valid()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_url(&self, path: &str) -> String {
|
|
||||||
self.build_url_with_version(path, crate::models::ApiVersion::V1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_url_with_version(&self, path: &str, version: crate::models::ApiVersion) -> String {
|
|
||||||
if path.starts_with("http") {
|
|
||||||
path.to_string()
|
|
||||||
} else {
|
|
||||||
let base = self.config.api_base_url.trim_end_matches('/');
|
|
||||||
let path = path.trim_start_matches('/');
|
|
||||||
let version_prefix = version.path_prefix();
|
|
||||||
|
|
||||||
if base.ends_with("/api") {
|
|
||||||
format!("{}/{}{}", base, version_prefix, path)
|
|
||||||
} else {
|
|
||||||
format!("{}/api/{}{}", base, version_prefix, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn add_auth_header(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
|
||||||
if let Some(token) = self.get_auth_token().await {
|
|
||||||
if token.is_valid() {
|
|
||||||
return builder.header("Authorization", format!("{} {}", token.token_type, token.token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event operations
|
|
||||||
pub async fn get_upcoming_events(&self, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
events::get_upcoming_events(self, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_events(&self, params: Option<PaginationParams>) -> Result<ApiListResponse<Event>> {
|
|
||||||
events::get_events(self, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_event(&self, id: &str) -> Result<Option<Event>> {
|
|
||||||
events::get_event(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_event(&self, event: NewEvent) -> Result<String> {
|
|
||||||
events::create_event(self, event).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_event(&self, id: &str, update: EventUpdate) -> Result<()> {
|
|
||||||
events::update_event(self, id, update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_event(&self, id: &str) -> Result<()> {
|
|
||||||
events::delete_event(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bulletin operations
|
|
||||||
pub async fn get_bulletins(&self, active_only: bool) -> Result<Vec<Bulletin>> {
|
|
||||||
bulletins::get_bulletins(self, active_only).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_current_bulletin(&self) -> Result<Option<Bulletin>> {
|
|
||||||
bulletins::get_current_bulletin(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_next_bulletin(&self) -> Result<Option<Bulletin>> {
|
|
||||||
bulletins::get_next_bulletin(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_bulletin(&self, id: &str) -> Result<Option<Bulletin>> {
|
|
||||||
bulletins::get_bulletin(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_bulletin(&self, bulletin: NewBulletin) -> Result<String> {
|
|
||||||
bulletins::create_bulletin(self, bulletin).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
pub async fn get_bulletins_v2(&self, params: Option<PaginationParams>) -> Result<ApiListResponse<Bulletin>> {
|
|
||||||
bulletins::get_bulletins_v2(self, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_current_bulletin_v2(&self) -> Result<Option<Bulletin>> {
|
|
||||||
bulletins::get_current_bulletin_v2(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_next_bulletin_v2(&self) -> Result<Option<Bulletin>> {
|
|
||||||
bulletins::get_next_bulletin_v2(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
pub async fn get_config(&self) -> Result<ChurchConfig> {
|
|
||||||
config::get_config(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_config_by_id(&self, record_id: &str) -> Result<ChurchConfig> {
|
|
||||||
config::get_config_by_id(self, record_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_config(&self, config: ChurchConfig) -> Result<()> {
|
|
||||||
config::update_config(self, config).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contact operations
|
|
||||||
pub async fn submit_contact_form(&self, form: ContactForm) -> Result<String> {
|
|
||||||
contact::submit_contact_form(self, form).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_contact_submissions(&self, params: Option<PaginationParams>) -> Result<ApiListResponse<ContactSubmission>> {
|
|
||||||
contact::get_contact_submissions(self, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_contact_submission(&self, id: &str) -> Result<Option<ContactSubmission>> {
|
|
||||||
contact::get_contact_submission(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_contact_submission(&self, id: &str, status: ContactStatus, response: Option<String>) -> Result<()> {
|
|
||||||
contact::update_contact_submission(self, id, status, response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sermon operations
|
|
||||||
pub async fn get_sermons(&self, params: Option<PaginationParams>) -> Result<ApiListResponse<Sermon>> {
|
|
||||||
sermons::get_sermons(self, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_sermons(&self, search: SermonSearch, params: Option<PaginationParams>) -> Result<ApiListResponse<Sermon>> {
|
|
||||||
sermons::search_sermons(self, search, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_sermon(&self, id: &str) -> Result<Option<Sermon>> {
|
|
||||||
sermons::get_sermon(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_featured_sermons(&self, limit: Option<u32>) -> Result<Vec<Sermon>> {
|
|
||||||
sermons::get_featured_sermons(self, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_recent_sermons(&self, limit: Option<u32>) -> Result<Vec<Sermon>> {
|
|
||||||
sermons::get_recent_sermons(self, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_sermon(&self, sermon: NewSermon) -> Result<String> {
|
|
||||||
sermons::create_sermon(self, sermon).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bible verse operations
|
|
||||||
pub async fn get_random_verse(&self) -> Result<BibleVerse> {
|
|
||||||
bible::get_random_verse(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_verse_of_the_day(&self) -> Result<VerseOfTheDay> {
|
|
||||||
bible::get_verse_of_the_day(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_verse_by_reference(&self, reference: &str) -> Result<Option<BibleVerse>> {
|
|
||||||
bible::get_verse_by_reference(self, reference).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_verses_by_category(&self, category: VerseCategory, limit: Option<u32>) -> Result<Vec<BibleVerse>> {
|
|
||||||
bible::get_verses_by_category(self, category, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_verses(&self, query: &str, limit: Option<u32>) -> Result<Vec<BibleVerse>> {
|
|
||||||
bible::search_verses(self, query, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 API methods
|
|
||||||
|
|
||||||
// Events V2
|
|
||||||
pub async fn get_events_v2(&self, params: Option<PaginationParams>) -> Result<ApiListResponse<Event>> {
|
|
||||||
events::get_events_v2(self, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_upcoming_events_v2(&self, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
events::get_upcoming_events_v2(self, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_featured_events_v2(&self, limit: Option<u32>) -> Result<Vec<Event>> {
|
|
||||||
events::get_featured_events_v2(self, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_event_v2(&self, id: &str) -> Result<Option<Event>> {
|
|
||||||
events::get_event_v2(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn submit_event(&self, submission: EventSubmission) -> Result<String> {
|
|
||||||
events::submit_event(self, submission).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bible V2
|
|
||||||
pub async fn get_random_verse_v2(&self) -> Result<BibleVerse> {
|
|
||||||
bible::get_random_verse_v2(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_bible_verses_v2(&self, params: Option<PaginationParams>) -> Result<ApiListResponse<BibleVerse>> {
|
|
||||||
bible::get_bible_verses_v2(self, params).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_verses_v2(&self, query: &str, limit: Option<u32>) -> Result<Vec<BibleVerse>> {
|
|
||||||
bible::search_verses_v2(self, query, limit).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contact V2
|
|
||||||
pub async fn submit_contact_form_v2(&self, form: ContactForm) -> Result<String> {
|
|
||||||
contact::submit_contact_form_v2(self, form).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config and Schedule V2
|
|
||||||
pub async fn get_config_v2(&self) -> Result<ChurchConfig> {
|
|
||||||
config::get_config_v2(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_schedule(&self, date: Option<&str>) -> Result<Schedule> {
|
|
||||||
config::get_schedule(self, date).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_schedule_v2(&self, date: Option<&str>) -> Result<Schedule> {
|
|
||||||
config::get_schedule_v2(self, date).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_conference_data(&self) -> Result<ConferenceData> {
|
|
||||||
config::get_conference_data(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_conference_data_v2(&self) -> Result<ConferenceData> {
|
|
||||||
config::get_conference_data_v2(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_livestreams(&self) -> Result<Vec<Sermon>> {
|
|
||||||
sermons::get_livestreams(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Owncast Live Streaming
|
|
||||||
pub async fn get_stream_status(&self) -> Result<livestream::StreamStatus> {
|
|
||||||
livestream::get_stream_status(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_live_stream(&self) -> Result<livestream::LiveStream> {
|
|
||||||
livestream::get_live_stream(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin operations
|
|
||||||
|
|
||||||
// Admin Bulletins
|
|
||||||
pub async fn create_admin_bulletin(&self, bulletin: NewBulletin) -> Result<String> {
|
|
||||||
admin::create_bulletin(self, bulletin).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_admin_bulletin(&self, id: &str, update: BulletinUpdate) -> Result<()> {
|
|
||||||
admin::update_bulletin(self, id, update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_admin_bulletin(&self, id: &str) -> Result<()> {
|
|
||||||
admin::delete_bulletin(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Events - Edit/delete allowed, creation via submission form only
|
|
||||||
pub async fn update_admin_event(&self, id: &str, update: EventUpdate) -> Result<()> {
|
|
||||||
admin::update_admin_event(self, id, update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_admin_event(&self, id: &str) -> Result<()> {
|
|
||||||
admin::delete_admin_event(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Pending Events
|
|
||||||
pub async fn get_pending_events(&self) -> Result<Vec<PendingEvent>> {
|
|
||||||
admin::get_pending_events(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn approve_pending_event(&self, id: &str) -> Result<()> {
|
|
||||||
admin::approve_pending_event(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reject_pending_event(&self, id: &str) -> Result<()> {
|
|
||||||
admin::reject_pending_event(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_pending_event(&self, id: &str) -> Result<()> {
|
|
||||||
admin::delete_pending_event(self, id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Users
|
|
||||||
pub async fn get_admin_users(&self) -> Result<Vec<User>> {
|
|
||||||
admin::get_users(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Schedule
|
|
||||||
pub async fn create_admin_schedule(&self, schedule: NewSchedule) -> Result<String> {
|
|
||||||
admin::create_schedule(self, schedule).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_admin_schedule(&self, date: &str, update: ScheduleUpdate) -> Result<()> {
|
|
||||||
admin::update_schedule(self, date, update).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_admin_schedule(&self, date: &str) -> Result<()> {
|
|
||||||
admin::delete_schedule(self, date).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_admin_schedules(&self) -> Result<Vec<Schedule>> {
|
|
||||||
admin::get_all_schedules(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_admin_config(&self) -> Result<ChurchConfig> {
|
|
||||||
admin::get_admin_config(self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// File Upload operations
|
|
||||||
pub async fn upload_bulletin_pdf(&self, bulletin_id: &str, file_data: Vec<u8>, filename: String) -> Result<UploadResponse> {
|
|
||||||
uploads::upload_bulletin_pdf(self, bulletin_id, file_data, filename).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upload_bulletin_cover(&self, bulletin_id: &str, file_data: Vec<u8>, filename: String) -> Result<UploadResponse> {
|
|
||||||
uploads::upload_bulletin_cover(self, bulletin_id, file_data, filename).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upload_event_image(&self, event_id: &str, file_data: Vec<u8>, filename: String) -> Result<UploadResponse> {
|
|
||||||
uploads::upload_event_image(self, event_id, file_data, filename).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
pub async fn health_check(&self) -> Result<bool> {
|
|
||||||
let url = self.build_url("/health");
|
|
||||||
let response = self.client.get(&url).send().await?;
|
|
||||||
Ok(response.status().is_success())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clear_cache(&self) {
|
|
||||||
self.cache.clear().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_cache_stats(&self) -> (usize, usize) {
|
|
||||||
(self.cache.len().await, self.config.max_cache_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Admin Auth operations
|
|
||||||
pub async fn admin_login(&self, email: &str, password: &str) -> Result<String> {
|
|
||||||
let url = self.build_url("/auth/login");
|
|
||||||
let request_body = serde_json::json!({
|
|
||||||
"email": email,
|
|
||||||
"password": password
|
|
||||||
});
|
|
||||||
|
|
||||||
let response = self.client
|
|
||||||
.post(&url)
|
|
||||||
.json(&request_body)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let auth_response: serde_json::Value = response.json().await?;
|
|
||||||
if let Some(token) = auth_response.get("token").and_then(|t| t.as_str()) {
|
|
||||||
Ok(token.to_string())
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api("No token in response".to_string()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
Err(crate::error::ChurchApiError::Api(error_text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn validate_admin_token(&self, token: &str) -> Result<bool> {
|
|
||||||
let url = self.build_url("/admin/events/pending");
|
|
||||||
let response = self.client
|
|
||||||
.get(&url)
|
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(response.status().is_success())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::{Sermon, ApiSermon, NewSermon, SermonSearch, PaginationParams, ApiListResponse, DeviceCapabilities},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_sermons(client: &ChurchApiClient, params: Option<PaginationParams>) -> Result<ApiListResponse<Sermon>> {
|
|
||||||
let mut path = "/sermons".to_string();
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(sort) = ¶ms.sort {
|
|
||||||
query_params.push(("sort", sort.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(filter) = ¶ms.filter {
|
|
||||||
query_params.push(("filter", filter.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_list(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_sermons(
|
|
||||||
client: &ChurchApiClient,
|
|
||||||
search: SermonSearch,
|
|
||||||
params: Option<PaginationParams>
|
|
||||||
) -> Result<ApiListResponse<Sermon>> {
|
|
||||||
let mut path = "/sermons/search".to_string();
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
|
|
||||||
if let Some(query) = &search.query {
|
|
||||||
query_params.push(("q", query.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(speaker) = &search.speaker {
|
|
||||||
query_params.push(("speaker", speaker.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(category) = &search.category {
|
|
||||||
query_params.push(("category", format!("{:?}", category).to_lowercase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(series) = &search.series {
|
|
||||||
query_params.push(("series", series.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(featured_only) = search.featured_only {
|
|
||||||
if featured_only {
|
|
||||||
query_params.push(("featured", "true".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(has_video) = search.has_video {
|
|
||||||
if has_video {
|
|
||||||
query_params.push(("has_video", "true".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(has_audio) = search.has_audio {
|
|
||||||
if has_audio {
|
|
||||||
query_params.push(("has_audio", "true".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(params) = params {
|
|
||||||
if let Some(page) = params.page {
|
|
||||||
query_params.push(("page", page.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(per_page) = params.per_page {
|
|
||||||
query_params.push(("per_page", per_page.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query_params.is_empty() {
|
|
||||||
let query_string = query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&");
|
|
||||||
path.push_str(&format!("?{}", query_string));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api_list(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_sermon(client: &ChurchApiClient, id: &str) -> Result<Option<Sermon>> {
|
|
||||||
let path = format!("/sermons/{}", id);
|
|
||||||
|
|
||||||
match client.get_api(&path).await {
|
|
||||||
Ok(sermon) => Ok(Some(sermon)),
|
|
||||||
Err(crate::error::ChurchApiError::NotFound) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_featured_sermons(client: &ChurchApiClient, limit: Option<u32>) -> Result<Vec<Sermon>> {
|
|
||||||
let mut path = "/sermons/featured".to_string();
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get_api(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert seconds to human readable duration
|
|
||||||
fn format_duration_seconds(seconds: u32) -> String {
|
|
||||||
let hours = seconds / 3600;
|
|
||||||
let minutes = (seconds % 3600) / 60;
|
|
||||||
let remaining_seconds = seconds % 60;
|
|
||||||
|
|
||||||
if hours > 0 {
|
|
||||||
format!("{}:{:02}:{:02}", hours, minutes, remaining_seconds)
|
|
||||||
} else {
|
|
||||||
format!("{}:{:02}", minutes, remaining_seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared function to convert API sermon/livestream data to Sermon model
|
|
||||||
fn convert_api_sermon_to_sermon(api_sermon: ApiSermon, category: crate::models::sermon::SermonCategory) -> Sermon {
|
|
||||||
// Parse date string to DateTime if available
|
|
||||||
let date = if let Some(date_str) = &api_sermon.date {
|
|
||||||
chrono::DateTime::parse_from_str(&format!("{} 00:00:00 +0000", date_str), "%Y-%m-%d %H:%M:%S %z")
|
|
||||||
.unwrap_or_else(|_| chrono::Utc::now().into())
|
|
||||||
.with_timezone(&chrono::Utc)
|
|
||||||
} else {
|
|
||||||
chrono::Utc::now()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Duration is already in string format from the API, so use it directly
|
|
||||||
let duration_string = Some(api_sermon.duration.clone());
|
|
||||||
|
|
||||||
// Generate optimal streaming URL for the device
|
|
||||||
let media_url = if !api_sermon.id.is_empty() {
|
|
||||||
let base_url = "https://api.rockvilletollandsda.church"; // TODO: Get from config
|
|
||||||
let streaming_url = DeviceCapabilities::get_optimal_streaming_url(base_url, &api_sermon.id);
|
|
||||||
Some(streaming_url.url)
|
|
||||||
} else {
|
|
||||||
api_sermon.video_url.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
Sermon {
|
|
||||||
id: api_sermon.id.clone(),
|
|
||||||
title: api_sermon.title,
|
|
||||||
speaker: api_sermon.speaker.unwrap_or("Unknown".to_string()),
|
|
||||||
description: api_sermon.description.unwrap_or_default(),
|
|
||||||
date,
|
|
||||||
scripture_reference: api_sermon.scripture_reading.unwrap_or_default(),
|
|
||||||
series: None,
|
|
||||||
duration_string,
|
|
||||||
media_url,
|
|
||||||
audio_url: api_sermon.audio_url,
|
|
||||||
video_url: api_sermon.video_url,
|
|
||||||
transcript: None,
|
|
||||||
thumbnail: api_sermon.thumbnail,
|
|
||||||
tags: None,
|
|
||||||
category,
|
|
||||||
is_featured: false,
|
|
||||||
view_count: 0,
|
|
||||||
download_count: 0,
|
|
||||||
created_at: date,
|
|
||||||
updated_at: date,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_recent_sermons(client: &ChurchApiClient, limit: Option<u32>) -> Result<Vec<Sermon>> {
|
|
||||||
let mut path = "/sermons".to_string();
|
|
||||||
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
path.push_str(&format!("?limit={}", limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The new API returns a wrapper with "sermons" array
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct SermonsResponse {
|
|
||||||
success: bool,
|
|
||||||
data: Vec<ApiSermon>,
|
|
||||||
message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: SermonsResponse = client.get(&path).await?;
|
|
||||||
|
|
||||||
// Convert using shared logic
|
|
||||||
let sermons = response.data.into_iter()
|
|
||||||
.map(|api_sermon| convert_api_sermon_to_sermon(api_sermon, crate::models::sermon::SermonCategory::Regular))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(sermons)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_sermon(client: &ChurchApiClient, sermon: NewSermon) -> Result<String> {
|
|
||||||
client.post_api("/sermons", &sermon).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Livestreams endpoint - reuses ApiSermon since format is identical
|
|
||||||
|
|
||||||
pub async fn get_livestreams(client: &ChurchApiClient) -> Result<Vec<Sermon>> {
|
|
||||||
// Use the new API endpoint for livestreams
|
|
||||||
let path = "/livestreams";
|
|
||||||
|
|
||||||
// The new API returns a wrapper with "data" array (same format as sermons endpoint)
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
|
||||||
struct LivestreamsResponse {
|
|
||||||
success: bool,
|
|
||||||
data: Vec<ApiSermon>,
|
|
||||||
message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: LivestreamsResponse = client.get(path).await?;
|
|
||||||
|
|
||||||
// Convert using shared logic - same as regular sermons but different category
|
|
||||||
let sermons = response.data.into_iter()
|
|
||||||
.map(|api_sermon| convert_api_sermon_to_sermon(api_sermon, crate::models::sermon::SermonCategory::LivestreamArchive))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(sermons)
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
use crate::{
|
|
||||||
client::ChurchApiClient,
|
|
||||||
error::Result,
|
|
||||||
models::UploadResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Upload PDF file for a bulletin
|
|
||||||
pub async fn upload_bulletin_pdf(
|
|
||||||
client: &ChurchApiClient,
|
|
||||||
bulletin_id: &str,
|
|
||||||
file_data: Vec<u8>,
|
|
||||||
filename: String,
|
|
||||||
) -> Result<UploadResponse> {
|
|
||||||
let path = format!("/upload/bulletins/{}/pdf", bulletin_id);
|
|
||||||
|
|
||||||
let url = client.build_url(&path);
|
|
||||||
|
|
||||||
let part = reqwest::multipart::Part::bytes(file_data)
|
|
||||||
.file_name(filename)
|
|
||||||
.mime_str("application/pdf")
|
|
||||||
.map_err(|e| crate::error::ChurchApiError::Internal(format!("Failed to create multipart: {}", e)))?;
|
|
||||||
|
|
||||||
let form = reqwest::multipart::Form::new()
|
|
||||||
.part("file", part);
|
|
||||||
|
|
||||||
let request = client.client.post(&url).multipart(form);
|
|
||||||
let request = client.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = request.send().await?;
|
|
||||||
let result: crate::models::ApiResponse<UploadResponse> = response.json().await?;
|
|
||||||
|
|
||||||
if result.success {
|
|
||||||
result.data.ok_or_else(|| {
|
|
||||||
crate::error::ChurchApiError::Api("File upload succeeded but no response returned".to_string())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(
|
|
||||||
result.error
|
|
||||||
.or(result.message)
|
|
||||||
.unwrap_or_else(|| "File upload failed".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload cover image for a bulletin
|
|
||||||
pub async fn upload_bulletin_cover(
|
|
||||||
client: &ChurchApiClient,
|
|
||||||
bulletin_id: &str,
|
|
||||||
file_data: Vec<u8>,
|
|
||||||
filename: String,
|
|
||||||
) -> Result<UploadResponse> {
|
|
||||||
let path = format!("/upload/bulletins/{}/cover", bulletin_id);
|
|
||||||
|
|
||||||
let url = client.build_url(&path);
|
|
||||||
|
|
||||||
let part = reqwest::multipart::Part::bytes(file_data)
|
|
||||||
.file_name(filename)
|
|
||||||
.mime_str("image/jpeg")
|
|
||||||
.map_err(|e| crate::error::ChurchApiError::Internal(format!("Failed to create multipart: {}", e)))?;
|
|
||||||
|
|
||||||
let form = reqwest::multipart::Form::new()
|
|
||||||
.part("file", part);
|
|
||||||
|
|
||||||
let request = client.client.post(&url).multipart(form);
|
|
||||||
let request = client.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = request.send().await?;
|
|
||||||
let result: crate::models::ApiResponse<UploadResponse> = response.json().await?;
|
|
||||||
|
|
||||||
if result.success {
|
|
||||||
result.data.ok_or_else(|| {
|
|
||||||
crate::error::ChurchApiError::Api("File upload succeeded but no response returned".to_string())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(
|
|
||||||
result.error
|
|
||||||
.or(result.message)
|
|
||||||
.unwrap_or_else(|| "File upload failed".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload image for an event
|
|
||||||
pub async fn upload_event_image(
|
|
||||||
client: &ChurchApiClient,
|
|
||||||
event_id: &str,
|
|
||||||
file_data: Vec<u8>,
|
|
||||||
filename: String,
|
|
||||||
) -> Result<UploadResponse> {
|
|
||||||
let path = format!("/upload/events/{}/image", event_id);
|
|
||||||
|
|
||||||
let url = client.build_url(&path);
|
|
||||||
|
|
||||||
let part = reqwest::multipart::Part::bytes(file_data)
|
|
||||||
.file_name(filename)
|
|
||||||
.mime_str("image/jpeg")
|
|
||||||
.map_err(|e| crate::error::ChurchApiError::Internal(format!("Failed to create multipart: {}", e)))?;
|
|
||||||
|
|
||||||
let form = reqwest::multipart::Form::new()
|
|
||||||
.part("file", part);
|
|
||||||
|
|
||||||
let request = client.client.post(&url).multipart(form);
|
|
||||||
let request = client.add_auth_header(request).await;
|
|
||||||
|
|
||||||
let response = request.send().await?;
|
|
||||||
let result: crate::models::ApiResponse<UploadResponse> = response.json().await?;
|
|
||||||
|
|
||||||
if result.success {
|
|
||||||
result.data.ok_or_else(|| {
|
|
||||||
crate::error::ChurchApiError::Api("File upload succeeded but no response returned".to_string())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(crate::error::ChurchApiError::Api(
|
|
||||||
result.error
|
|
||||||
.or(result.message)
|
|
||||||
.unwrap_or_else(|| "File upload failed".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ChurchCoreConfig {
|
|
||||||
pub api_base_url: String,
|
|
||||||
pub cache_ttl: Duration,
|
|
||||||
pub timeout: Duration,
|
|
||||||
pub connect_timeout: Duration,
|
|
||||||
pub retry_attempts: u32,
|
|
||||||
pub enable_offline_mode: bool,
|
|
||||||
pub max_cache_size: usize,
|
|
||||||
pub user_agent: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ChurchCoreConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
api_base_url: "https://api.rockvilletollandsda.church".to_string(),
|
|
||||||
cache_ttl: Duration::from_secs(300), // 5 minutes
|
|
||||||
timeout: Duration::from_secs(10),
|
|
||||||
connect_timeout: Duration::from_secs(5),
|
|
||||||
retry_attempts: 3,
|
|
||||||
enable_offline_mode: true,
|
|
||||||
max_cache_size: 1000,
|
|
||||||
user_agent: format!("church-core/{}", env!("CARGO_PKG_VERSION")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChurchCoreConfig {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
|
|
||||||
self.api_base_url = url.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
|
|
||||||
self.cache_ttl = ttl;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
|
||||||
self.timeout = timeout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_retry_attempts(mut self, attempts: u32) -> Self {
|
|
||||||
self.retry_attempts = attempts;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_offline_mode(mut self, enabled: bool) -> Self {
|
|
||||||
self.enable_offline_mode = enabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_max_cache_size(mut self, size: usize) -> Self {
|
|
||||||
self.max_cache_size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_user_agent(mut self, agent: impl Into<String>) -> Self {
|
|
||||||
self.user_agent = agent.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, ChurchApiError>;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ChurchApiError {
|
|
||||||
#[error("HTTP request failed: {0}")]
|
|
||||||
Http(#[from] reqwest::Error),
|
|
||||||
|
|
||||||
#[error("JSON parsing failed: {0}")]
|
|
||||||
Json(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
#[error("Date parsing failed: {0}")]
|
|
||||||
DateParse(String),
|
|
||||||
|
|
||||||
#[error("API returned error: {0}")]
|
|
||||||
Api(String),
|
|
||||||
|
|
||||||
#[error("Authentication failed: {0}")]
|
|
||||||
Auth(String),
|
|
||||||
|
|
||||||
#[error("Cache error: {0}")]
|
|
||||||
Cache(String),
|
|
||||||
|
|
||||||
#[error("Validation error: {0}")]
|
|
||||||
Validation(String),
|
|
||||||
|
|
||||||
#[error("Configuration error: {0}")]
|
|
||||||
Config(String),
|
|
||||||
|
|
||||||
#[error("Network error: {0}")]
|
|
||||||
Network(String),
|
|
||||||
|
|
||||||
#[error("Timeout error: operation took too long")]
|
|
||||||
Timeout,
|
|
||||||
|
|
||||||
#[error("Rate limit exceeded")]
|
|
||||||
RateLimit,
|
|
||||||
|
|
||||||
#[error("Resource not found")]
|
|
||||||
NotFound,
|
|
||||||
|
|
||||||
#[error("Permission denied")]
|
|
||||||
PermissionDenied,
|
|
||||||
|
|
||||||
#[error("Internal error: {0}")]
|
|
||||||
Internal(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChurchApiError {
|
|
||||||
pub fn is_network_error(&self) -> bool {
|
|
||||||
matches!(self, Self::Http(_) | Self::Network(_) | Self::Timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_auth_error(&self) -> bool {
|
|
||||||
matches!(self, Self::Auth(_) | Self::PermissionDenied)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_temporary(&self) -> bool {
|
|
||||||
matches!(self, Self::Timeout | Self::RateLimit | Self::Network(_))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// FFI module for church-core
|
|
||||||
// This module is only compiled when the ffi feature is enabled
|
|
||||||
|
|
||||||
use crate::{ChurchApiClient, ChurchCoreConfig, ChurchApiError};
|
|
||||||
|
|
||||||
// Re-export for UniFFI
|
|
||||||
pub use crate::{
|
|
||||||
models::*,
|
|
||||||
ChurchApiClient,
|
|
||||||
ChurchCoreConfig,
|
|
||||||
ChurchApiError,
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
pub mod client;
|
|
||||||
pub mod models;
|
|
||||||
pub mod auth;
|
|
||||||
pub mod cache;
|
|
||||||
pub mod utils;
|
|
||||||
pub mod error;
|
|
||||||
pub mod config;
|
|
||||||
pub mod api;
|
|
||||||
pub use client::ChurchApiClient;
|
|
||||||
pub use config::ChurchCoreConfig;
|
|
||||||
pub use error::{ChurchApiError, Result};
|
|
||||||
pub use models::*;
|
|
||||||
pub use cache::*;
|
|
||||||
pub use api::*;
|
|
||||||
|
|
||||||
#[cfg(feature = "wasm")]
|
|
||||||
pub mod wasm;
|
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// User information for admin user management
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct User {
|
|
||||||
pub id: String,
|
|
||||||
pub username: String,
|
|
||||||
pub email: Option<String>,
|
|
||||||
pub role: AdminUserRole,
|
|
||||||
pub is_active: bool,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
pub last_login: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum AdminUserRole {
|
|
||||||
#[serde(rename = "admin")]
|
|
||||||
Admin,
|
|
||||||
#[serde(rename = "moderator")]
|
|
||||||
Moderator,
|
|
||||||
#[serde(rename = "user")]
|
|
||||||
User,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schedule data
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Schedule {
|
|
||||||
pub id: String,
|
|
||||||
pub date: String, // YYYY-MM-DD format
|
|
||||||
pub sabbath_school: Option<String>,
|
|
||||||
pub divine_worship: Option<String>,
|
|
||||||
pub scripture_reading: Option<String>,
|
|
||||||
pub sunset: Option<String>,
|
|
||||||
pub special_notes: Option<String>,
|
|
||||||
// Personnel assignments
|
|
||||||
pub song_leader: Option<String>,
|
|
||||||
pub ss_teacher: Option<String>,
|
|
||||||
pub ss_leader: Option<String>,
|
|
||||||
pub mission_story: Option<String>,
|
|
||||||
pub special_program: Option<String>,
|
|
||||||
pub sermon_speaker: Option<String>,
|
|
||||||
pub scripture: Option<String>,
|
|
||||||
pub offering: Option<String>,
|
|
||||||
pub deacons: Option<String>,
|
|
||||||
pub special_music: Option<String>,
|
|
||||||
pub childrens_story: Option<String>,
|
|
||||||
pub afternoon_program: Option<String>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Conference data
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ConferenceData {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub website: Option<String>,
|
|
||||||
pub contact_info: Option<String>,
|
|
||||||
pub leadership: Option<Vec<ConferenceLeader>>,
|
|
||||||
pub announcements: Option<Vec<String>>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ConferenceLeader {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
pub email: Option<String>,
|
|
||||||
pub phone: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// New schedule creation
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct NewSchedule {
|
|
||||||
pub date: String, // YYYY-MM-DD format
|
|
||||||
pub sabbath_school: Option<String>,
|
|
||||||
pub divine_worship: Option<String>,
|
|
||||||
pub scripture_reading: Option<String>,
|
|
||||||
pub sunset: Option<String>,
|
|
||||||
pub special_notes: Option<String>,
|
|
||||||
// Personnel assignments
|
|
||||||
pub song_leader: Option<String>,
|
|
||||||
pub ss_teacher: Option<String>,
|
|
||||||
pub ss_leader: Option<String>,
|
|
||||||
pub mission_story: Option<String>,
|
|
||||||
pub special_program: Option<String>,
|
|
||||||
pub sermon_speaker: Option<String>,
|
|
||||||
pub scripture: Option<String>,
|
|
||||||
pub offering: Option<String>,
|
|
||||||
pub deacons: Option<String>,
|
|
||||||
pub special_music: Option<String>,
|
|
||||||
pub childrens_story: Option<String>,
|
|
||||||
pub afternoon_program: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schedule update
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ScheduleUpdate {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sabbath_school: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub divine_worship: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub scripture_reading: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sunset: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_notes: Option<String>,
|
|
||||||
// Personnel assignments
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub song_leader: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub ss_teacher: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub ss_leader: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mission_story: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_program: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sermon_speaker: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub scripture: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub offering: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub deacons: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_music: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub childrens_story: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub afternoon_program: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// File upload response
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct UploadResponse {
|
|
||||||
pub file_path: String,
|
|
||||||
pub pdf_path: Option<String>, // Full URL to the uploaded file
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
|
@ -1,276 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct AuthToken {
|
|
||||||
pub token: String,
|
|
||||||
pub token_type: String,
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
pub user_id: Option<String>,
|
|
||||||
pub user_name: Option<String>,
|
|
||||||
pub user_email: Option<String>,
|
|
||||||
pub permissions: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
pub identity: String, // email or username
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub token: String,
|
|
||||||
pub user: AuthUser,
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct AuthUser {
|
|
||||||
pub id: String,
|
|
||||||
pub email: String,
|
|
||||||
pub name: String,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub verified: bool,
|
|
||||||
pub role: UserRole,
|
|
||||||
pub permissions: Vec<String>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
pub last_login: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct RefreshTokenRequest {
|
|
||||||
pub refresh_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct RefreshTokenResponse {
|
|
||||||
pub token: String,
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PasswordResetRequest {
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PasswordResetConfirm {
|
|
||||||
pub token: String,
|
|
||||||
pub new_password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ChangePasswordRequest {
|
|
||||||
pub current_password: String,
|
|
||||||
pub new_password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct RegisterRequest {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
pub name: String,
|
|
||||||
pub username: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct EmailVerificationRequest {
|
|
||||||
pub token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum UserRole {
|
|
||||||
#[serde(rename = "admin")]
|
|
||||||
Admin,
|
|
||||||
#[serde(rename = "pastor")]
|
|
||||||
Pastor,
|
|
||||||
#[serde(rename = "elder")]
|
|
||||||
Elder,
|
|
||||||
#[serde(rename = "deacon")]
|
|
||||||
Deacon,
|
|
||||||
#[serde(rename = "ministry_leader")]
|
|
||||||
MinistryLeader,
|
|
||||||
#[serde(rename = "member")]
|
|
||||||
Member,
|
|
||||||
#[serde(rename = "visitor")]
|
|
||||||
Visitor,
|
|
||||||
#[serde(rename = "guest")]
|
|
||||||
Guest,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PocketBaseAuthResponse {
|
|
||||||
pub token: String,
|
|
||||||
pub record: PocketBaseUser,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PocketBaseUser {
|
|
||||||
pub id: String,
|
|
||||||
pub email: String,
|
|
||||||
pub name: String,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
pub verified: bool,
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
pub updated: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl AuthToken {
|
|
||||||
pub fn is_expired(&self) -> bool {
|
|
||||||
Utc::now() > self.expires_at
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_valid(&self) -> bool {
|
|
||||||
!self.is_expired() && !self.token.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expires_in_seconds(&self) -> i64 {
|
|
||||||
(self.expires_at - Utc::now()).num_seconds().max(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expires_in_minutes(&self) -> i64 {
|
|
||||||
(self.expires_at - Utc::now()).num_minutes().max(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_permission(&self, permission: &str) -> bool {
|
|
||||||
self.permissions.contains(&permission.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_any_permission(&self, permissions: &[&str]) -> bool {
|
|
||||||
permissions.iter().any(|p| self.has_permission(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_all_permissions(&self, permissions: &[&str]) -> bool {
|
|
||||||
permissions.iter().all(|p| self.has_permission(p))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthUser {
|
|
||||||
pub fn is_admin(&self) -> bool {
|
|
||||||
matches!(self.role, UserRole::Admin)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_pastor(&self) -> bool {
|
|
||||||
matches!(self.role, UserRole::Pastor)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_leadership(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.role,
|
|
||||||
UserRole::Admin | UserRole::Pastor | UserRole::Elder | UserRole::Deacon
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_member(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.role,
|
|
||||||
UserRole::Admin
|
|
||||||
| UserRole::Pastor
|
|
||||||
| UserRole::Elder
|
|
||||||
| UserRole::Deacon
|
|
||||||
| UserRole::MinistryLeader
|
|
||||||
| UserRole::Member
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_edit_content(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.role,
|
|
||||||
UserRole::Admin | UserRole::Pastor | UserRole::Elder | UserRole::MinistryLeader
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_moderate(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.role,
|
|
||||||
UserRole::Admin | UserRole::Pastor | UserRole::Elder | UserRole::Deacon
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_name(&self) -> String {
|
|
||||||
if !self.name.is_empty() {
|
|
||||||
self.name.clone()
|
|
||||||
} else if let Some(username) = &self.username {
|
|
||||||
username.clone()
|
|
||||||
} else {
|
|
||||||
self.email.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserRole {
|
|
||||||
pub fn display_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
UserRole::Admin => "Administrator",
|
|
||||||
UserRole::Pastor => "Pastor",
|
|
||||||
UserRole::Elder => "Elder",
|
|
||||||
UserRole::Deacon => "Deacon",
|
|
||||||
UserRole::MinistryLeader => "Ministry Leader",
|
|
||||||
UserRole::Member => "Member",
|
|
||||||
UserRole::Visitor => "Visitor",
|
|
||||||
UserRole::Guest => "Guest",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn permissions(&self) -> Vec<&'static str> {
|
|
||||||
match self {
|
|
||||||
UserRole::Admin => vec![
|
|
||||||
"admin.*",
|
|
||||||
"events.*",
|
|
||||||
"bulletins.*",
|
|
||||||
"sermons.*",
|
|
||||||
"contacts.*",
|
|
||||||
"users.*",
|
|
||||||
"config.*",
|
|
||||||
],
|
|
||||||
UserRole::Pastor => vec![
|
|
||||||
"events.*",
|
|
||||||
"bulletins.*",
|
|
||||||
"sermons.*",
|
|
||||||
"contacts.read",
|
|
||||||
"contacts.respond",
|
|
||||||
"users.read",
|
|
||||||
],
|
|
||||||
UserRole::Elder => vec![
|
|
||||||
"events.read",
|
|
||||||
"events.create",
|
|
||||||
"bulletins.read",
|
|
||||||
"sermons.read",
|
|
||||||
"contacts.read",
|
|
||||||
"contacts.respond",
|
|
||||||
],
|
|
||||||
UserRole::Deacon => vec![
|
|
||||||
"events.read",
|
|
||||||
"bulletins.read",
|
|
||||||
"sermons.read",
|
|
||||||
"contacts.read",
|
|
||||||
],
|
|
||||||
UserRole::MinistryLeader => vec![
|
|
||||||
"events.read",
|
|
||||||
"events.create",
|
|
||||||
"bulletins.read",
|
|
||||||
"sermons.read",
|
|
||||||
],
|
|
||||||
UserRole::Member => vec![
|
|
||||||
"events.read",
|
|
||||||
"bulletins.read",
|
|
||||||
"sermons.read",
|
|
||||||
],
|
|
||||||
UserRole::Visitor => vec![
|
|
||||||
"events.read",
|
|
||||||
"bulletins.read",
|
|
||||||
"sermons.read",
|
|
||||||
],
|
|
||||||
UserRole::Guest => vec![
|
|
||||||
"events.read",
|
|
||||||
"bulletins.read",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct BibleVerse {
|
|
||||||
pub text: String,
|
|
||||||
pub reference: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub version: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub book: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub chapter: Option<u32>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub verse: Option<u32>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub category: Option<VerseCategory>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct VerseOfTheDay {
|
|
||||||
pub verse: BibleVerse,
|
|
||||||
pub date: chrono::NaiveDate,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub commentary: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub theme: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum VerseCategory {
|
|
||||||
#[serde(rename = "comfort")]
|
|
||||||
Comfort,
|
|
||||||
#[serde(rename = "hope")]
|
|
||||||
Hope,
|
|
||||||
#[serde(rename = "faith")]
|
|
||||||
Faith,
|
|
||||||
#[serde(rename = "love")]
|
|
||||||
Love,
|
|
||||||
#[serde(rename = "peace")]
|
|
||||||
Peace,
|
|
||||||
#[serde(rename = "strength")]
|
|
||||||
Strength,
|
|
||||||
#[serde(rename = "wisdom")]
|
|
||||||
Wisdom,
|
|
||||||
#[serde(rename = "guidance")]
|
|
||||||
Guidance,
|
|
||||||
#[serde(rename = "forgiveness")]
|
|
||||||
Forgiveness,
|
|
||||||
#[serde(rename = "salvation")]
|
|
||||||
Salvation,
|
|
||||||
#[serde(rename = "prayer")]
|
|
||||||
Prayer,
|
|
||||||
#[serde(rename = "praise")]
|
|
||||||
Praise,
|
|
||||||
#[serde(rename = "thanksgiving")]
|
|
||||||
Thanksgiving,
|
|
||||||
#[serde(rename = "other")]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BibleVerse {
|
|
||||||
pub fn new(text: String, reference: String) -> Self {
|
|
||||||
Self {
|
|
||||||
text,
|
|
||||||
reference,
|
|
||||||
version: None,
|
|
||||||
book: None,
|
|
||||||
chapter: None,
|
|
||||||
verse: None,
|
|
||||||
category: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_version(mut self, version: String) -> Self {
|
|
||||||
self.version = Some(version);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_book(mut self, book: String) -> Self {
|
|
||||||
self.book = Some(book);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_location(mut self, chapter: u32, verse: u32) -> Self {
|
|
||||||
self.chapter = Some(chapter);
|
|
||||||
self.verse = Some(verse);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_category(mut self, category: VerseCategory) -> Self {
|
|
||||||
self.category = Some(category);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VerseOfTheDay {
|
|
||||||
pub fn new(verse: BibleVerse, date: chrono::NaiveDate) -> Self {
|
|
||||||
Self {
|
|
||||||
verse,
|
|
||||||
date,
|
|
||||||
commentary: None,
|
|
||||||
theme: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_commentary(mut self, commentary: String) -> Self {
|
|
||||||
self.commentary = Some(commentary);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_theme(mut self, theme: String) -> Self {
|
|
||||||
self.theme = Some(theme);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VerseCategory {
|
|
||||||
pub fn display_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
VerseCategory::Comfort => "Comfort",
|
|
||||||
VerseCategory::Hope => "Hope",
|
|
||||||
VerseCategory::Faith => "Faith",
|
|
||||||
VerseCategory::Love => "Love",
|
|
||||||
VerseCategory::Peace => "Peace",
|
|
||||||
VerseCategory::Strength => "Strength",
|
|
||||||
VerseCategory::Wisdom => "Wisdom",
|
|
||||||
VerseCategory::Guidance => "Guidance",
|
|
||||||
VerseCategory::Forgiveness => "Forgiveness",
|
|
||||||
VerseCategory::Salvation => "Salvation",
|
|
||||||
VerseCategory::Prayer => "Prayer",
|
|
||||||
VerseCategory::Praise => "Praise",
|
|
||||||
VerseCategory::Thanksgiving => "Thanksgiving",
|
|
||||||
VerseCategory::Other => "Other",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,240 +0,0 @@
|
||||||
use chrono::{DateTime, NaiveDate, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Bulletin {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub date: NaiveDate,
|
|
||||||
pub sabbath_school: String,
|
|
||||||
pub divine_worship: String,
|
|
||||||
pub scripture_reading: String,
|
|
||||||
pub sunset: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub pdf_path: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub cover_image: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_active: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub announcements: Option<Vec<Announcement>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub hymns: Option<Vec<BulletinHymn>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_music: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub offering_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sermon_title: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub speaker: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub liturgy: Option<Vec<LiturgyItem>>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct NewBulletin {
|
|
||||||
pub title: String,
|
|
||||||
pub date: NaiveDate,
|
|
||||||
pub sabbath_school: String,
|
|
||||||
pub divine_worship: String,
|
|
||||||
pub scripture_reading: String,
|
|
||||||
pub sunset: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub pdf_path: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub cover_image: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_active: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub announcements: Option<Vec<Announcement>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub hymns: Option<Vec<BulletinHymn>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_music: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub offering_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sermon_title: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub speaker: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub liturgy: Option<Vec<LiturgyItem>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct BulletinUpdate {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub title: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub date: Option<NaiveDate>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sabbath_school: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub divine_worship: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub scripture_reading: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sunset: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub pdf_path: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub cover_image: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub is_active: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub announcements: Option<Vec<Announcement>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub hymns: Option<Vec<BulletinHymn>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_music: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub offering_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub sermon_title: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub speaker: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub liturgy: Option<Vec<LiturgyItem>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Announcement {
|
|
||||||
pub id: Option<String>,
|
|
||||||
pub title: String,
|
|
||||||
pub content: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub category: Option<AnnouncementCategory>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_urgent: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_info: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub expires_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct BulletinHymn {
|
|
||||||
pub number: u32,
|
|
||||||
pub title: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub category: Option<HymnCategory>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub verses: Option<Vec<u32>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct LiturgyItem {
|
|
||||||
pub order: u32,
|
|
||||||
pub item_type: LiturgyType,
|
|
||||||
pub title: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub leader: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub scripture_reference: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub hymn_number: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum AnnouncementCategory {
|
|
||||||
#[serde(rename = "general")]
|
|
||||||
General,
|
|
||||||
#[serde(rename = "ministry")]
|
|
||||||
Ministry,
|
|
||||||
#[serde(rename = "social")]
|
|
||||||
Social,
|
|
||||||
#[serde(rename = "urgent")]
|
|
||||||
Urgent,
|
|
||||||
#[serde(rename = "prayer")]
|
|
||||||
Prayer,
|
|
||||||
#[serde(rename = "community")]
|
|
||||||
Community,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum HymnCategory {
|
|
||||||
#[serde(rename = "opening")]
|
|
||||||
Opening,
|
|
||||||
#[serde(rename = "closing")]
|
|
||||||
Closing,
|
|
||||||
#[serde(rename = "offertory")]
|
|
||||||
Offertory,
|
|
||||||
#[serde(rename = "communion")]
|
|
||||||
Communion,
|
|
||||||
#[serde(rename = "special")]
|
|
||||||
Special,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum LiturgyType {
|
|
||||||
#[serde(rename = "prelude")]
|
|
||||||
Prelude,
|
|
||||||
#[serde(rename = "welcome")]
|
|
||||||
Welcome,
|
|
||||||
#[serde(rename = "opening_hymn")]
|
|
||||||
OpeningHymn,
|
|
||||||
#[serde(rename = "prayer")]
|
|
||||||
Prayer,
|
|
||||||
#[serde(rename = "scripture")]
|
|
||||||
Scripture,
|
|
||||||
#[serde(rename = "children_story")]
|
|
||||||
ChildrenStory,
|
|
||||||
#[serde(rename = "hymn")]
|
|
||||||
Hymn,
|
|
||||||
#[serde(rename = "offertory")]
|
|
||||||
Offertory,
|
|
||||||
#[serde(rename = "sermon")]
|
|
||||||
Sermon,
|
|
||||||
#[serde(rename = "closing_hymn")]
|
|
||||||
ClosingHymn,
|
|
||||||
#[serde(rename = "benediction")]
|
|
||||||
Benediction,
|
|
||||||
#[serde(rename = "postlude")]
|
|
||||||
Postlude,
|
|
||||||
#[serde(rename = "announcements")]
|
|
||||||
Announcements,
|
|
||||||
#[serde(rename = "special_music")]
|
|
||||||
SpecialMusic,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bulletin {
|
|
||||||
pub fn has_pdf(&self) -> bool {
|
|
||||||
self.pdf_path.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_cover_image(&self) -> bool {
|
|
||||||
self.cover_image.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_announcements(&self) -> Vec<&Announcement> {
|
|
||||||
self.announcements
|
|
||||||
.as_ref()
|
|
||||||
.map(|announcements| {
|
|
||||||
announcements
|
|
||||||
.iter()
|
|
||||||
.filter(|announcement| {
|
|
||||||
announcement.expires_at
|
|
||||||
.map_or(true, |expires| expires > Utc::now())
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn urgent_announcements(&self) -> Vec<&Announcement> {
|
|
||||||
self.announcements
|
|
||||||
.as_ref()
|
|
||||||
.map(|announcements| {
|
|
||||||
announcements
|
|
||||||
.iter()
|
|
||||||
.filter(|announcement| announcement.is_urgent)
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,344 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::models::event::{Event, RecurringType};
|
|
||||||
use crate::models::bulletin::Bulletin;
|
|
||||||
use crate::models::sermon::Sermon;
|
|
||||||
use chrono::{DateTime, Utc, Local, Timelike};
|
|
||||||
|
|
||||||
/// Client-facing Event model with both raw timestamps and formatted display strings
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ClientEvent {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
// Raw ISO timestamps for calendar/system APIs
|
|
||||||
#[serde(rename = "start_time")]
|
|
||||||
pub start_time: String, // ISO timestamp like "2025-08-13T05:00:00-04:00"
|
|
||||||
#[serde(rename = "end_time")]
|
|
||||||
pub end_time: String, // ISO timestamp like "2025-08-13T06:00:00-04:00"
|
|
||||||
|
|
||||||
// Formatted display strings for UI
|
|
||||||
#[serde(rename = "formatted_time")]
|
|
||||||
pub formatted_time: String, // "6:00 PM - 8:00 PM"
|
|
||||||
#[serde(rename = "formatted_date")]
|
|
||||||
pub formatted_date: String, // "Friday, August 15, 2025"
|
|
||||||
#[serde(rename = "formatted_date_time")]
|
|
||||||
pub formatted_date_time: String, // "Friday, August 15, 2025 at 6:00 PM"
|
|
||||||
|
|
||||||
// Additional display fields for UI components
|
|
||||||
#[serde(rename = "day_of_month")]
|
|
||||||
pub day_of_month: String, // "15"
|
|
||||||
#[serde(rename = "month_abbreviation")]
|
|
||||||
pub month_abbreviation: String, // "AUG"
|
|
||||||
#[serde(rename = "time_string")]
|
|
||||||
pub time_string: String, // "6:00 PM - 8:00 PM" (alias for formatted_time)
|
|
||||||
#[serde(rename = "is_multi_day")]
|
|
||||||
pub is_multi_day: bool, // true if event spans multiple days
|
|
||||||
#[serde(rename = "detailed_time_display")]
|
|
||||||
pub detailed_time_display: String, // Full time range for detail views
|
|
||||||
|
|
||||||
pub location: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "location_url")]
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub image: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub category: String,
|
|
||||||
#[serde(rename = "is_featured")]
|
|
||||||
pub is_featured: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "recurring_type")]
|
|
||||||
pub recurring_type: Option<String>,
|
|
||||||
#[serde(rename = "created_at")]
|
|
||||||
pub created_at: String, // ISO timestamp
|
|
||||||
#[serde(rename = "updated_at")]
|
|
||||||
pub updated_at: String, // ISO timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to format time range from DateTime objects in local timezone
|
|
||||||
fn format_time_range_from_datetime(start_time: &DateTime<Utc>, end_time: &DateTime<Utc>) -> String {
|
|
||||||
// Convert UTC to local timezone for display
|
|
||||||
let start_local = start_time.with_timezone(&Local);
|
|
||||||
let end_local = end_time.with_timezone(&Local);
|
|
||||||
|
|
||||||
// Use consistent formatting: always show hour without leading zero, include minutes, use PM/AM
|
|
||||||
let start_formatted = if start_local.minute() == 0 {
|
|
||||||
start_local.format("%l %p").to_string().trim().to_string()
|
|
||||||
} else {
|
|
||||||
start_local.format("%l:%M %p").to_string().trim().to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let end_formatted = if end_local.minute() == 0 {
|
|
||||||
end_local.format("%l %p").to_string().trim().to_string()
|
|
||||||
} else {
|
|
||||||
end_local.format("%l:%M %p").to_string().trim().to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
// If start and end times are the same, just show one time
|
|
||||||
if start_formatted == end_formatted {
|
|
||||||
start_formatted
|
|
||||||
} else {
|
|
||||||
format!("{} - {}", start_formatted, end_formatted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Event> for ClientEvent {
|
|
||||||
fn from(event: Event) -> Self {
|
|
||||||
let description = event.clean_description();
|
|
||||||
let category = event.category.to_string();
|
|
||||||
let recurring_type = event.recurring_type.as_ref().map(|rt| rt.to_string());
|
|
||||||
|
|
||||||
// Raw ISO timestamps for calendar/system APIs
|
|
||||||
let start_time = event.start_time.format("%Y-%m-%dT%H:%M:%S%z").to_string();
|
|
||||||
let end_time = event.end_time.format("%Y-%m-%dT%H:%M:%S%z").to_string();
|
|
||||||
|
|
||||||
// Generate formatted display strings in local timezone
|
|
||||||
let start_local = event.start_time.with_timezone(&Local);
|
|
||||||
let end_local = event.end_time.with_timezone(&Local);
|
|
||||||
|
|
||||||
// Check if event spans multiple days
|
|
||||||
let is_multi_day = start_local.date_naive() != end_local.date_naive();
|
|
||||||
|
|
||||||
let (formatted_time, formatted_date, formatted_date_time, day_of_month, month_abbreviation, time_string, detailed_time_display) = if is_multi_day {
|
|
||||||
// Multi-day event: show date range for formatted_date, but start time for simplified views
|
|
||||||
let start_date = start_local.format("%B %d, %Y").to_string();
|
|
||||||
let end_date = end_local.format("%B %d, %Y").to_string();
|
|
||||||
let formatted_date = format!("{} - {}", start_date, end_date);
|
|
||||||
|
|
||||||
// For detailed view: show full date range with full time range
|
|
||||||
let time_range = format_time_range_from_datetime(&event.start_time, &event.end_time);
|
|
||||||
let formatted_time = format!("{} - {}, {}",
|
|
||||||
start_local.format("%b %d").to_string(),
|
|
||||||
end_local.format("%b %d").to_string(),
|
|
||||||
time_range
|
|
||||||
);
|
|
||||||
|
|
||||||
// For HomeFeed simplified view: just show start time
|
|
||||||
let start_time_formatted = if start_local.minute() == 0 {
|
|
||||||
start_local.format("%l %p").to_string().trim().to_string()
|
|
||||||
} else {
|
|
||||||
start_local.format("%l:%M %p").to_string().trim().to_string()
|
|
||||||
};
|
|
||||||
let time_string = start_time_formatted;
|
|
||||||
|
|
||||||
// For detail views: use the same time_range that eliminates redundancy
|
|
||||||
let detailed_time_display = time_range.clone();
|
|
||||||
|
|
||||||
let formatted_date_time = format!("{} - {}", start_date, end_date);
|
|
||||||
|
|
||||||
// Use start date for calendar display
|
|
||||||
let day_of_month = start_local.format("%d").to_string().trim_start_matches('0').to_string();
|
|
||||||
let month_abbreviation = start_local.format("%b").to_string().to_uppercase();
|
|
||||||
|
|
||||||
(formatted_time, formatted_date, formatted_date_time, day_of_month, month_abbreviation, time_string, detailed_time_display)
|
|
||||||
} else {
|
|
||||||
// Single day event: show time range
|
|
||||||
let formatted_time = format_time_range_from_datetime(&event.start_time, &event.end_time);
|
|
||||||
let formatted_date = start_local.format("%B %d, %Y").to_string();
|
|
||||||
// Use consistent time formatting for single events too
|
|
||||||
let time_formatted = if start_local.minute() == 0 {
|
|
||||||
start_local.format("%l %p").to_string().trim().to_string()
|
|
||||||
} else {
|
|
||||||
start_local.format("%l:%M %p").to_string().trim().to_string()
|
|
||||||
};
|
|
||||||
let formatted_date_time = format!("{} at {}", formatted_date, time_formatted);
|
|
||||||
|
|
||||||
let day_of_month = start_local.format("%d").to_string().trim_start_matches('0').to_string();
|
|
||||||
let month_abbreviation = start_local.format("%b").to_string().to_uppercase();
|
|
||||||
|
|
||||||
// For single events, time_string should just be start time for HomeFeed
|
|
||||||
let time_string = time_formatted;
|
|
||||||
|
|
||||||
// For single events, detailed_time_display is same as formatted_time
|
|
||||||
let detailed_time_display = formatted_time.clone();
|
|
||||||
|
|
||||||
(formatted_time, formatted_date, formatted_date_time, day_of_month, month_abbreviation, time_string, detailed_time_display)
|
|
||||||
};
|
|
||||||
|
|
||||||
let created_at = event.created_at.format("%Y-%m-%dT%H:%M:%S%z").to_string();
|
|
||||||
let updated_at = event.updated_at.format("%Y-%m-%dT%H:%M:%S%z").to_string();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: event.id,
|
|
||||||
title: event.title,
|
|
||||||
description,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
formatted_time,
|
|
||||||
formatted_date,
|
|
||||||
formatted_date_time,
|
|
||||||
day_of_month,
|
|
||||||
month_abbreviation,
|
|
||||||
time_string,
|
|
||||||
is_multi_day,
|
|
||||||
detailed_time_display,
|
|
||||||
location: event.location,
|
|
||||||
location_url: event.location_url,
|
|
||||||
image: event.image,
|
|
||||||
thumbnail: event.thumbnail,
|
|
||||||
category,
|
|
||||||
is_featured: event.is_featured,
|
|
||||||
recurring_type,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client-facing Bulletin model with formatted dates
|
|
||||||
/// Serializes to camelCase JSON for iOS compatibility
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ClientBulletin {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub date: String, // Pre-formatted date string
|
|
||||||
#[serde(rename = "sabbathSchool")]
|
|
||||||
pub sabbath_school: String,
|
|
||||||
#[serde(rename = "divineWorship")]
|
|
||||||
pub divine_worship: String,
|
|
||||||
#[serde(rename = "scriptureReading")]
|
|
||||||
pub scripture_reading: String,
|
|
||||||
pub sunset: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "pdfPath")]
|
|
||||||
pub pdf_path: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "coverImage")]
|
|
||||||
pub cover_image: Option<String>,
|
|
||||||
#[serde(rename = "isActive")]
|
|
||||||
pub is_active: bool,
|
|
||||||
// Add other fields as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Bulletin> for ClientBulletin {
|
|
||||||
fn from(bulletin: Bulletin) -> Self {
|
|
||||||
Self {
|
|
||||||
id: bulletin.id,
|
|
||||||
title: bulletin.title,
|
|
||||||
date: bulletin.date.format("%A, %B %d, %Y").to_string(), // Format NaiveDate to string
|
|
||||||
sabbath_school: bulletin.sabbath_school,
|
|
||||||
divine_worship: bulletin.divine_worship,
|
|
||||||
scripture_reading: bulletin.scripture_reading,
|
|
||||||
sunset: bulletin.sunset,
|
|
||||||
pdf_path: bulletin.pdf_path,
|
|
||||||
cover_image: bulletin.cover_image,
|
|
||||||
is_active: bulletin.is_active,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client-facing Sermon model with pre-formatted dates and cleaned data
|
|
||||||
/// Serializes to camelCase JSON for iOS compatibility
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ClientSermon {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub speaker: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub date: Option<String>, // Pre-formatted date string
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "audioUrl")]
|
|
||||||
pub audio_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "videoUrl")]
|
|
||||||
pub video_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub duration: Option<String>, // Pre-formatted duration
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "mediaType")]
|
|
||||||
pub media_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub image: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", rename = "scriptureReading")]
|
|
||||||
pub scripture_reading: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientSermon {
|
|
||||||
/// Create a ClientSermon from a Sermon with URL conversion using base API URL
|
|
||||||
pub fn from_sermon_with_base_url(sermon: Sermon, base_url: &str) -> Self {
|
|
||||||
let date = sermon.date.format("%B %d, %Y").to_string();
|
|
||||||
let media_type = if sermon.has_video() {
|
|
||||||
Some("Video".to_string())
|
|
||||||
} else if sermon.has_audio() {
|
|
||||||
Some("Audio".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to convert relative URLs to full URLs
|
|
||||||
let make_full_url = |url: Option<String>| -> Option<String> {
|
|
||||||
url.map(|u| {
|
|
||||||
if u.starts_with("http://") || u.starts_with("https://") {
|
|
||||||
// Already a full URL
|
|
||||||
u
|
|
||||||
} else if u.starts_with("/") {
|
|
||||||
// Relative URL starting with /
|
|
||||||
let base = base_url.trim_end_matches('/');
|
|
||||||
format!("{}{}", base, u)
|
|
||||||
} else {
|
|
||||||
// Relative URL not starting with /
|
|
||||||
let base = base_url.trim_end_matches('/');
|
|
||||||
format!("{}/{}", base, u)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: sermon.id,
|
|
||||||
title: sermon.title,
|
|
||||||
speaker: sermon.speaker,
|
|
||||||
description: Some(sermon.description),
|
|
||||||
date: Some(date),
|
|
||||||
audio_url: make_full_url(sermon.audio_url),
|
|
||||||
video_url: make_full_url(sermon.video_url),
|
|
||||||
duration: sermon.duration_string, // Use raw duration string from API
|
|
||||||
media_type,
|
|
||||||
thumbnail: make_full_url(sermon.thumbnail),
|
|
||||||
image: None, // Sermons don't have separate image field
|
|
||||||
scripture_reading: if sermon.scripture_reference.is_empty() { None } else { Some(sermon.scripture_reference.clone()) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Sermon> for ClientSermon {
|
|
||||||
fn from(sermon: Sermon) -> Self {
|
|
||||||
let date = sermon.date.format("%B %d, %Y").to_string();
|
|
||||||
let media_type = if sermon.has_video() {
|
|
||||||
Some("Video".to_string())
|
|
||||||
} else if sermon.has_audio() {
|
|
||||||
Some("Audio".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: sermon.id,
|
|
||||||
title: sermon.title,
|
|
||||||
speaker: sermon.speaker,
|
|
||||||
description: Some(sermon.description),
|
|
||||||
date: Some(date),
|
|
||||||
audio_url: sermon.audio_url,
|
|
||||||
video_url: sermon.video_url,
|
|
||||||
duration: sermon.duration_string, // Use raw duration string from API
|
|
||||||
media_type,
|
|
||||||
thumbnail: sermon.thumbnail,
|
|
||||||
image: None, // Sermons don't have separate image field
|
|
||||||
scripture_reading: if sermon.scripture_reference.is_empty() { None } else { Some(sermon.scripture_reference.clone()) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ToString implementations for enums if not already present
|
|
||||||
impl ToString for RecurringType {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
RecurringType::Daily => "Daily".to_string(),
|
|
||||||
RecurringType::Weekly => "Weekly".to_string(),
|
|
||||||
RecurringType::Biweekly => "Bi-weekly".to_string(),
|
|
||||||
RecurringType::Monthly => "Monthly".to_string(),
|
|
||||||
RecurringType::FirstTuesday => "First Tuesday".to_string(),
|
|
||||||
RecurringType::FirstSabbath => "First Sabbath".to_string(),
|
|
||||||
RecurringType::LastSabbath => "Last Sabbath".to_string(),
|
|
||||||
RecurringType::SecondThirdSaturday => "2nd/3rd Saturday Monthly".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ApiResponse<T> {
|
|
||||||
pub success: bool,
|
|
||||||
pub data: Option<T>,
|
|
||||||
pub message: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ApiListResponse<T> {
|
|
||||||
pub success: bool,
|
|
||||||
pub data: ApiListData<T>,
|
|
||||||
pub message: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ApiListData<T> {
|
|
||||||
pub items: Vec<T>,
|
|
||||||
pub total: u32,
|
|
||||||
pub page: u32,
|
|
||||||
pub per_page: u32,
|
|
||||||
pub has_more: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PaginationParams {
|
|
||||||
pub page: Option<u32>,
|
|
||||||
pub per_page: Option<u32>,
|
|
||||||
pub sort: Option<String>,
|
|
||||||
pub filter: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PaginationParams {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
page: Some(1),
|
|
||||||
per_page: Some(50),
|
|
||||||
sort: None,
|
|
||||||
filter: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginationParams {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_page(mut self, page: u32) -> Self {
|
|
||||||
self.page = Some(page);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_per_page(mut self, per_page: u32) -> Self {
|
|
||||||
self.per_page = Some(per_page);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_sort(mut self, sort: impl Into<String>) -> Self {
|
|
||||||
self.sort = Some(sort.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_filter(mut self, filter: impl Into<String>) -> Self {
|
|
||||||
self.filter = Some(filter.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,253 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Coordinates {
|
|
||||||
pub lat: f64,
|
|
||||||
pub lng: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ChurchConfig {
|
|
||||||
pub church_name: Option<String>,
|
|
||||||
pub church_address: Option<String>,
|
|
||||||
pub po_box: Option<String>,
|
|
||||||
pub contact_phone: Option<String>,
|
|
||||||
pub contact_email: Option<String>,
|
|
||||||
pub website_url: Option<String>,
|
|
||||||
pub google_maps_url: Option<String>,
|
|
||||||
pub facebook_url: Option<String>,
|
|
||||||
pub youtube_url: Option<String>,
|
|
||||||
pub instagram_url: Option<String>,
|
|
||||||
pub about_text: Option<String>,
|
|
||||||
pub mission_statement: Option<String>,
|
|
||||||
pub tagline: Option<String>,
|
|
||||||
pub brand_color: Option<String>,
|
|
||||||
pub donation_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub service_times: Option<Vec<ServiceTime>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub pastoral_staff: Option<Vec<StaffMember>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub ministries: Option<Vec<Ministry>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub app_settings: Option<AppSettings>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub emergency_contacts: Option<Vec<EmergencyContact>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub coordinates: Option<Coordinates>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ServiceTime {
|
|
||||||
pub day: String,
|
|
||||||
pub service: String,
|
|
||||||
pub time: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ServiceTimes {
|
|
||||||
pub sabbath_school: Option<String>,
|
|
||||||
pub divine_worship: Option<String>,
|
|
||||||
pub prayer_meeting: Option<String>,
|
|
||||||
pub youth_service: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub special_services: Option<Vec<SpecialService>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SpecialService {
|
|
||||||
pub name: String,
|
|
||||||
pub time: String,
|
|
||||||
pub frequency: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct StaffMember {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub email: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub phone: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub photo: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub bio: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub responsibilities: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Ministry {
|
|
||||||
pub name: String,
|
|
||||||
pub description: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub leader: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_email: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_phone: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub meeting_time: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub meeting_location: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub website: Option<String>,
|
|
||||||
pub category: MinistryCategory,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct EmergencyContact {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
pub phone: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub email: Option<String>,
|
|
||||||
pub priority: u32,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub availability: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct AppSettings {
|
|
||||||
pub enable_notifications: bool,
|
|
||||||
pub enable_calendar_sync: bool,
|
|
||||||
pub enable_offline_mode: bool,
|
|
||||||
pub theme: AppTheme,
|
|
||||||
pub default_language: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub owncast_server: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub bible_version: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub hymnal_version: Option<String>,
|
|
||||||
pub cache_duration_minutes: u32,
|
|
||||||
pub auto_refresh_interval_minutes: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum MinistryCategory {
|
|
||||||
#[serde(rename = "worship")]
|
|
||||||
Worship,
|
|
||||||
#[serde(rename = "education")]
|
|
||||||
Education,
|
|
||||||
#[serde(rename = "youth")]
|
|
||||||
Youth,
|
|
||||||
#[serde(rename = "children")]
|
|
||||||
Children,
|
|
||||||
#[serde(rename = "outreach")]
|
|
||||||
Outreach,
|
|
||||||
#[serde(rename = "health")]
|
|
||||||
Health,
|
|
||||||
#[serde(rename = "music")]
|
|
||||||
Music,
|
|
||||||
#[serde(rename = "fellowship")]
|
|
||||||
Fellowship,
|
|
||||||
#[serde(rename = "prayer")]
|
|
||||||
Prayer,
|
|
||||||
#[serde(rename = "stewardship")]
|
|
||||||
Stewardship,
|
|
||||||
#[serde(rename = "other")]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum AppTheme {
|
|
||||||
#[serde(rename = "light")]
|
|
||||||
Light,
|
|
||||||
#[serde(rename = "dark")]
|
|
||||||
Dark,
|
|
||||||
#[serde(rename = "system")]
|
|
||||||
System,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppSettings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enable_notifications: true,
|
|
||||||
enable_calendar_sync: true,
|
|
||||||
enable_offline_mode: true,
|
|
||||||
theme: AppTheme::System,
|
|
||||||
default_language: "en".to_string(),
|
|
||||||
owncast_server: None,
|
|
||||||
bible_version: Some("KJV".to_string()),
|
|
||||||
hymnal_version: Some("1985".to_string()),
|
|
||||||
cache_duration_minutes: 60,
|
|
||||||
auto_refresh_interval_minutes: 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChurchConfig {
|
|
||||||
pub fn get_display_name(&self) -> String {
|
|
||||||
self.church_name
|
|
||||||
.as_ref()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "Church".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_social_media(&self) -> bool {
|
|
||||||
self.facebook_url.is_some() || self.youtube_url.is_some() || self.instagram_url.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_contact_info(&self) -> Vec<(String, String)> {
|
|
||||||
let mut contacts = Vec::new();
|
|
||||||
|
|
||||||
if let Some(phone) = &self.contact_phone {
|
|
||||||
contacts.push(("Phone".to_string(), phone.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(email) = &self.contact_email {
|
|
||||||
contacts.push(("Email".to_string(), email.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(address) = &self.church_address {
|
|
||||||
contacts.push(("Address".to_string(), address.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(po_box) = &self.po_box {
|
|
||||||
contacts.push(("PO Box".to_string(), po_box.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
contacts
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_ministries(&self) -> Vec<&Ministry> {
|
|
||||||
self.ministries
|
|
||||||
.as_ref()
|
|
||||||
.map(|ministries| {
|
|
||||||
ministries
|
|
||||||
.iter()
|
|
||||||
.filter(|ministry| ministry.is_active)
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ministries_by_category(&self, category: MinistryCategory) -> Vec<&Ministry> {
|
|
||||||
self.ministries
|
|
||||||
.as_ref()
|
|
||||||
.map(|ministries| {
|
|
||||||
ministries
|
|
||||||
.iter()
|
|
||||||
.filter(|ministry| ministry.category == category && ministry.is_active)
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emergency_contacts_by_priority(&self) -> Vec<&EmergencyContact> {
|
|
||||||
self.emergency_contacts
|
|
||||||
.as_ref()
|
|
||||||
.map(|contacts| {
|
|
||||||
let mut sorted = contacts.iter().collect::<Vec<_>>();
|
|
||||||
sorted.sort_by_key(|contact| contact.priority);
|
|
||||||
sorted
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,339 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ContactForm {
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub phone: Option<String>,
|
|
||||||
pub subject: String,
|
|
||||||
pub message: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub category: Option<ContactCategory>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub preferred_contact_method: Option<ContactMethod>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub urgent: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub visitor_info: Option<VisitorInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ContactSubmission {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub phone: Option<String>,
|
|
||||||
pub subject: String,
|
|
||||||
pub message: String,
|
|
||||||
pub category: ContactCategory,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub preferred_contact_method: Option<ContactMethod>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub urgent: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub visitor_info: Option<VisitorInfo>,
|
|
||||||
pub status: ContactStatus,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub assigned_to: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub response: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub responded_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct VisitorInfo {
|
|
||||||
pub is_first_time: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub how_heard_about_us: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub interests: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub family_members: Option<Vec<FamilyMember>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub prayer_requests: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub address: Option<Address>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub wants_follow_up: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub wants_newsletter: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct FamilyMember {
|
|
||||||
pub name: String,
|
|
||||||
pub relationship: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub age_group: Option<AgeGroup>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub interests: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Address {
|
|
||||||
pub street: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub city: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub state: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub zip_code: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub country: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PrayerRequest {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub email: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub phone: Option<String>,
|
|
||||||
pub request: String,
|
|
||||||
pub category: PrayerCategory,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_public: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_urgent: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_confidential: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub follow_up_requested: Option<bool>,
|
|
||||||
pub status: PrayerStatus,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub assigned_to: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub notes: Option<String>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub answered_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum ContactCategory {
|
|
||||||
#[serde(rename = "general")]
|
|
||||||
General,
|
|
||||||
#[serde(rename = "pastoral_care")]
|
|
||||||
PastoralCare,
|
|
||||||
#[serde(rename = "prayer_request")]
|
|
||||||
PrayerRequest,
|
|
||||||
#[serde(rename = "visitor")]
|
|
||||||
Visitor,
|
|
||||||
#[serde(rename = "ministry")]
|
|
||||||
Ministry,
|
|
||||||
#[serde(rename = "event")]
|
|
||||||
Event,
|
|
||||||
#[serde(rename = "technical")]
|
|
||||||
Technical,
|
|
||||||
#[serde(rename = "feedback")]
|
|
||||||
Feedback,
|
|
||||||
#[serde(rename = "donation")]
|
|
||||||
Donation,
|
|
||||||
#[serde(rename = "membership")]
|
|
||||||
Membership,
|
|
||||||
#[serde(rename = "baptism")]
|
|
||||||
Baptism,
|
|
||||||
#[serde(rename = "wedding")]
|
|
||||||
Wedding,
|
|
||||||
#[serde(rename = "funeral")]
|
|
||||||
Funeral,
|
|
||||||
#[serde(rename = "other")]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum ContactMethod {
|
|
||||||
#[serde(rename = "email")]
|
|
||||||
Email,
|
|
||||||
#[serde(rename = "phone")]
|
|
||||||
Phone,
|
|
||||||
#[serde(rename = "text")]
|
|
||||||
Text,
|
|
||||||
#[serde(rename = "mail")]
|
|
||||||
Mail,
|
|
||||||
#[serde(rename = "no_preference")]
|
|
||||||
NoPreference,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum ContactStatus {
|
|
||||||
#[serde(rename = "new")]
|
|
||||||
New,
|
|
||||||
#[serde(rename = "assigned")]
|
|
||||||
Assigned,
|
|
||||||
#[serde(rename = "in_progress")]
|
|
||||||
InProgress,
|
|
||||||
#[serde(rename = "responded")]
|
|
||||||
Responded,
|
|
||||||
#[serde(rename = "follow_up")]
|
|
||||||
FollowUp,
|
|
||||||
#[serde(rename = "completed")]
|
|
||||||
Completed,
|
|
||||||
#[serde(rename = "closed")]
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum PrayerCategory {
|
|
||||||
#[serde(rename = "health")]
|
|
||||||
Health,
|
|
||||||
#[serde(rename = "family")]
|
|
||||||
Family,
|
|
||||||
#[serde(rename = "finances")]
|
|
||||||
Finances,
|
|
||||||
#[serde(rename = "relationships")]
|
|
||||||
Relationships,
|
|
||||||
#[serde(rename = "spiritual")]
|
|
||||||
Spiritual,
|
|
||||||
#[serde(rename = "work")]
|
|
||||||
Work,
|
|
||||||
#[serde(rename = "travel")]
|
|
||||||
Travel,
|
|
||||||
#[serde(rename = "community")]
|
|
||||||
Community,
|
|
||||||
#[serde(rename = "praise")]
|
|
||||||
Praise,
|
|
||||||
#[serde(rename = "other")]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum PrayerStatus {
|
|
||||||
#[serde(rename = "new")]
|
|
||||||
New,
|
|
||||||
#[serde(rename = "praying")]
|
|
||||||
Praying,
|
|
||||||
#[serde(rename = "answered")]
|
|
||||||
Answered,
|
|
||||||
#[serde(rename = "ongoing")]
|
|
||||||
Ongoing,
|
|
||||||
#[serde(rename = "closed")]
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum AgeGroup {
|
|
||||||
#[serde(rename = "infant")]
|
|
||||||
Infant,
|
|
||||||
#[serde(rename = "toddler")]
|
|
||||||
Toddler,
|
|
||||||
#[serde(rename = "child")]
|
|
||||||
Child,
|
|
||||||
#[serde(rename = "youth")]
|
|
||||||
Youth,
|
|
||||||
#[serde(rename = "adult")]
|
|
||||||
Adult,
|
|
||||||
#[serde(rename = "senior")]
|
|
||||||
Senior,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactForm {
|
|
||||||
pub fn new(name: String, email: String, subject: String, message: String) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
phone: None,
|
|
||||||
subject,
|
|
||||||
message,
|
|
||||||
category: None,
|
|
||||||
preferred_contact_method: None,
|
|
||||||
urgent: None,
|
|
||||||
visitor_info: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_category(mut self, category: ContactCategory) -> Self {
|
|
||||||
self.category = Some(category);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_phone(mut self, phone: String) -> Self {
|
|
||||||
self.phone = Some(phone);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_preferred_method(mut self, method: ContactMethod) -> Self {
|
|
||||||
self.preferred_contact_method = Some(method);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark_urgent(mut self) -> Self {
|
|
||||||
self.urgent = Some(true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_visitor_info(mut self, visitor_info: VisitorInfo) -> Self {
|
|
||||||
self.visitor_info = Some(visitor_info);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_urgent(&self) -> bool {
|
|
||||||
self.urgent.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_visitor(&self) -> bool {
|
|
||||||
self.visitor_info.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactSubmission {
|
|
||||||
pub fn is_urgent(&self) -> bool {
|
|
||||||
self.urgent
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_visitor(&self) -> bool {
|
|
||||||
self.visitor_info.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_open(&self) -> bool {
|
|
||||||
!matches!(self.status, ContactStatus::Completed | ContactStatus::Closed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn needs_response(&self) -> bool {
|
|
||||||
matches!(self.status, ContactStatus::New | ContactStatus::Assigned)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn response_time(&self) -> Option<chrono::Duration> {
|
|
||||||
self.responded_at.map(|responded| responded - self.created_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn age_days(&self) -> i64 {
|
|
||||||
(Utc::now() - self.created_at).num_days()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrayerRequest {
|
|
||||||
pub fn is_urgent(&self) -> bool {
|
|
||||||
self.is_urgent
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_confidential(&self) -> bool {
|
|
||||||
self.is_confidential
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_public(&self) -> bool {
|
|
||||||
self.is_public && !self.is_confidential
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_open(&self) -> bool {
|
|
||||||
!matches!(self.status, PrayerStatus::Answered | PrayerStatus::Closed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_answered(&self) -> bool {
|
|
||||||
matches!(self.status, PrayerStatus::Answered)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn age_days(&self) -> i64 {
|
|
||||||
(Utc::now() - self.created_at).num_days()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,349 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize, Deserializer};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Timezone-aware timestamp from v2 API
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct TimezoneTimestamp {
|
|
||||||
pub utc: DateTime<Utc>,
|
|
||||||
pub local: String, // "2025-08-13T05:00:00-04:00"
|
|
||||||
pub timezone: String, // "America/New_York"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Custom deserializer that handles both v1 (simple string) and v2 (timezone object) formats
|
|
||||||
fn deserialize_flexible_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::{self, Visitor};
|
|
||||||
|
|
||||||
struct FlexibleDateTimeVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for FlexibleDateTimeVisitor {
|
|
||||||
type Value = DateTime<Utc>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("a string timestamp or timezone object")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
// v1 format: simple ISO string
|
|
||||||
DateTime::parse_from_rfc3339(value)
|
|
||||||
.map(|dt| dt.with_timezone(&Utc))
|
|
||||||
.map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
|
||||||
where
|
|
||||||
M: de::MapAccess<'de>,
|
|
||||||
{
|
|
||||||
// v2 format: timezone object - extract UTC field
|
|
||||||
let mut utc_value: Option<DateTime<Utc>> = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key::<String>()? {
|
|
||||||
match key.as_str() {
|
|
||||||
"utc" => {
|
|
||||||
utc_value = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Skip other fields (local, timezone)
|
|
||||||
let _: serde_json::Value = map.next_value()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
utc_value.ok_or_else(|| de::Error::missing_field("utc"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(FlexibleDateTimeVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Event {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
#[serde(deserialize_with = "deserialize_flexible_datetime")]
|
|
||||||
pub start_time: DateTime<Utc>,
|
|
||||||
#[serde(deserialize_with = "deserialize_flexible_datetime")]
|
|
||||||
pub end_time: DateTime<Utc>,
|
|
||||||
pub location: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub image: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub category: EventCategory,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_featured: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub recurring_type: Option<RecurringType>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_email: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_phone: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub registration_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub max_attendees: Option<u32>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub current_attendees: Option<u32>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub timezone: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub approved_from: Option<String>,
|
|
||||||
#[serde(deserialize_with = "deserialize_flexible_datetime")]
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
#[serde(deserialize_with = "deserialize_flexible_datetime")]
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct NewEvent {
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub start_time: DateTime<Utc>,
|
|
||||||
pub end_time: DateTime<Utc>,
|
|
||||||
pub location: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub image: Option<String>,
|
|
||||||
pub category: EventCategory,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_featured: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub recurring_type: Option<RecurringType>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_email: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_phone: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub registration_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub max_attendees: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct EventUpdate {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub title: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub start_time: Option<DateTime<Utc>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub end_time: Option<DateTime<Utc>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub location: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub image: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub category: Option<EventCategory>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub is_featured: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub recurring_type: Option<RecurringType>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_email: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub contact_phone: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub registration_url: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub max_attendees: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum EventCategory {
|
|
||||||
#[serde(rename = "service", alias = "Service")]
|
|
||||||
Service,
|
|
||||||
#[serde(rename = "ministry", alias = "Ministry")]
|
|
||||||
Ministry,
|
|
||||||
#[serde(rename = "social", alias = "Social")]
|
|
||||||
Social,
|
|
||||||
#[serde(rename = "education", alias = "Education")]
|
|
||||||
Education,
|
|
||||||
#[serde(rename = "outreach", alias = "Outreach")]
|
|
||||||
Outreach,
|
|
||||||
#[serde(rename = "youth", alias = "Youth")]
|
|
||||||
Youth,
|
|
||||||
#[serde(rename = "music", alias = "Music")]
|
|
||||||
Music,
|
|
||||||
#[serde(rename = "other", alias = "Other")]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum RecurringType {
|
|
||||||
#[serde(rename = "daily", alias = "DAILY")]
|
|
||||||
Daily,
|
|
||||||
#[serde(rename = "weekly", alias = "WEEKLY")]
|
|
||||||
Weekly,
|
|
||||||
#[serde(rename = "biweekly", alias = "BIWEEKLY")]
|
|
||||||
Biweekly,
|
|
||||||
#[serde(rename = "monthly", alias = "MONTHLY")]
|
|
||||||
Monthly,
|
|
||||||
#[serde(rename = "first_tuesday", alias = "FIRST_TUESDAY")]
|
|
||||||
FirstTuesday,
|
|
||||||
#[serde(rename = "first_sabbath", alias = "FIRST_SABBATH")]
|
|
||||||
FirstSabbath,
|
|
||||||
#[serde(rename = "last_sabbath", alias = "LAST_SABBATH")]
|
|
||||||
LastSabbath,
|
|
||||||
#[serde(rename = "2nd/3rd Saturday Monthly")]
|
|
||||||
SecondThirdSaturday,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
pub fn duration_minutes(&self) -> i64 {
|
|
||||||
(self.end_time - self.start_time).num_minutes()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_registration(&self) -> bool {
|
|
||||||
self.registration_url.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_full(&self) -> bool {
|
|
||||||
match (self.max_attendees, self.current_attendees) {
|
|
||||||
(Some(max), Some(current)) => current >= max,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spots_remaining(&self) -> Option<u32> {
|
|
||||||
match (self.max_attendees, self.current_attendees) {
|
|
||||||
(Some(max), Some(current)) => Some(max.saturating_sub(current)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for EventCategory {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
EventCategory::Service => write!(f, "Service"),
|
|
||||||
EventCategory::Ministry => write!(f, "Ministry"),
|
|
||||||
EventCategory::Social => write!(f, "Social"),
|
|
||||||
EventCategory::Education => write!(f, "Education"),
|
|
||||||
EventCategory::Outreach => write!(f, "Outreach"),
|
|
||||||
EventCategory::Youth => write!(f, "Youth"),
|
|
||||||
EventCategory::Music => write!(f, "Music"),
|
|
||||||
EventCategory::Other => write!(f, "Other"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
pub fn formatted_date(&self) -> String {
|
|
||||||
self.start_time.format("%A, %B %d, %Y").to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns formatted date range for multi-day events, single date for same-day events
|
|
||||||
pub fn formatted_date_range(&self) -> String {
|
|
||||||
let start_date = self.start_time.date_naive();
|
|
||||||
let end_date = self.end_time.date_naive();
|
|
||||||
|
|
||||||
if start_date == end_date {
|
|
||||||
// Same day event
|
|
||||||
self.start_time.format("%A, %B %d, %Y").to_string()
|
|
||||||
} else {
|
|
||||||
// Multi-day event
|
|
||||||
let start_formatted = self.start_time.format("%A, %B %d, %Y").to_string();
|
|
||||||
let end_formatted = self.end_time.format("%A, %B %d, %Y").to_string();
|
|
||||||
format!("{} - {}", start_formatted, end_formatted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn formatted_start_time(&self) -> String {
|
|
||||||
// Convert UTC to user's local timezone automatically
|
|
||||||
let local_time = self.start_time.with_timezone(&chrono::Local);
|
|
||||||
local_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn formatted_end_time(&self) -> String {
|
|
||||||
// Convert UTC to user's local timezone automatically
|
|
||||||
let local_time = self.end_time.with_timezone(&chrono::Local);
|
|
||||||
local_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clean_description(&self) -> String {
|
|
||||||
html2text::from_read(self.description.as_bytes(), 80)
|
|
||||||
.replace('\n', " ")
|
|
||||||
.split_whitespace()
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event submission for public submission endpoint
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct EventSubmission {
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub start_time: String, // ISO string format
|
|
||||||
pub end_time: String, // ISO string format
|
|
||||||
pub location: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
pub category: String, // String to match API exactly
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_featured: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub recurring_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub bulletin_week: Option<String>, // Date string in YYYY-MM-DD format
|
|
||||||
pub submitter_email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventSubmission {
|
|
||||||
/// Parse start_time string to DateTime<Utc>
|
|
||||||
pub fn parse_start_time(&self) -> Option<DateTime<Utc>> {
|
|
||||||
crate::utils::parse_datetime_flexible(&self.start_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse end_time string to DateTime<Utc>
|
|
||||||
pub fn parse_end_time(&self) -> Option<DateTime<Utc>> {
|
|
||||||
crate::utils::parse_datetime_flexible(&self.end_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate that both start and end times can be parsed
|
|
||||||
pub fn validate_times(&self) -> bool {
|
|
||||||
self.parse_start_time().is_some() && self.parse_end_time().is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pending event for admin management
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PendingEvent {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub start_time: DateTime<Utc>,
|
|
||||||
pub end_time: DateTime<Utc>,
|
|
||||||
pub location: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
pub category: EventCategory,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_featured: bool,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub recurring_type: Option<RecurringType>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub bulletin_week: Option<String>,
|
|
||||||
pub submitter_email: String,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
pub mod common;
|
|
||||||
pub mod event;
|
|
||||||
pub mod bulletin;
|
|
||||||
pub mod config;
|
|
||||||
pub mod contact;
|
|
||||||
pub mod sermon;
|
|
||||||
pub mod streaming;
|
|
||||||
pub mod auth;
|
|
||||||
pub mod bible;
|
|
||||||
pub mod client_models;
|
|
||||||
pub mod v2;
|
|
||||||
pub mod admin;
|
|
||||||
|
|
||||||
pub use common::*;
|
|
||||||
pub use event::*;
|
|
||||||
pub use bulletin::*;
|
|
||||||
pub use config::*;
|
|
||||||
pub use contact::*;
|
|
||||||
pub use sermon::*;
|
|
||||||
pub use streaming::*;
|
|
||||||
pub use auth::*;
|
|
||||||
pub use bible::*;
|
|
||||||
pub use client_models::*;
|
|
||||||
pub use v2::*;
|
|
||||||
pub use admin::*;
|
|
||||||
|
|
||||||
// Re-export livestream types from client module for convenience
|
|
||||||
pub use crate::client::livestream::{StreamStatus, LiveStream};
|
|
|
@ -1,376 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// API response structure for sermons from the external API
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ApiSermon {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub speaker: Option<String>,
|
|
||||||
pub date: Option<String>,
|
|
||||||
pub duration: String, // Duration as string like "1:13:01"
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub audio_url: Option<String>,
|
|
||||||
pub video_url: Option<String>,
|
|
||||||
pub media_type: Option<String>,
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub scripture_reading: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Sermon {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub speaker: String,
|
|
||||||
pub description: String,
|
|
||||||
pub date: DateTime<Utc>,
|
|
||||||
pub scripture_reference: String,
|
|
||||||
pub series: Option<String>,
|
|
||||||
pub duration_string: Option<String>, // Raw duration from API (e.g., "2:34:49")
|
|
||||||
pub media_url: Option<String>,
|
|
||||||
pub audio_url: Option<String>,
|
|
||||||
pub video_url: Option<String>,
|
|
||||||
pub transcript: Option<String>,
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
pub category: SermonCategory,
|
|
||||||
pub is_featured: bool,
|
|
||||||
pub view_count: u32,
|
|
||||||
pub download_count: u32,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct NewSermon {
|
|
||||||
pub title: String,
|
|
||||||
pub speaker: String,
|
|
||||||
pub description: String,
|
|
||||||
pub date: DateTime<Utc>,
|
|
||||||
pub scripture_reference: String,
|
|
||||||
pub series: Option<String>,
|
|
||||||
pub duration_string: Option<String>,
|
|
||||||
pub media_url: Option<String>,
|
|
||||||
pub audio_url: Option<String>,
|
|
||||||
pub video_url: Option<String>,
|
|
||||||
pub transcript: Option<String>,
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
pub category: SermonCategory,
|
|
||||||
pub is_featured: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SermonSeries {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub speaker: String,
|
|
||||||
pub start_date: DateTime<Utc>,
|
|
||||||
pub end_date: Option<DateTime<Utc>>,
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub sermons: Vec<Sermon>,
|
|
||||||
pub is_active: bool,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SermonNote {
|
|
||||||
pub id: String,
|
|
||||||
pub sermon_id: String,
|
|
||||||
pub user_id: String,
|
|
||||||
pub content: String,
|
|
||||||
pub timestamp_seconds: Option<u32>,
|
|
||||||
pub is_private: bool,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SermonFeedback {
|
|
||||||
pub id: String,
|
|
||||||
pub sermon_id: String,
|
|
||||||
pub user_name: Option<String>,
|
|
||||||
pub user_email: Option<String>,
|
|
||||||
pub rating: Option<u8>, // 1-5 stars
|
|
||||||
pub comment: Option<String>,
|
|
||||||
pub is_public: bool,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
pub enum SermonCategory {
|
|
||||||
#[serde(rename = "regular")]
|
|
||||||
Regular,
|
|
||||||
#[serde(rename = "evangelistic")]
|
|
||||||
Evangelistic,
|
|
||||||
#[serde(rename = "youth")]
|
|
||||||
Youth,
|
|
||||||
#[serde(rename = "children")]
|
|
||||||
Children,
|
|
||||||
#[serde(rename = "special")]
|
|
||||||
Special,
|
|
||||||
#[serde(rename = "prophecy")]
|
|
||||||
Prophecy,
|
|
||||||
#[serde(rename = "health")]
|
|
||||||
Health,
|
|
||||||
#[serde(rename = "stewardship")]
|
|
||||||
Stewardship,
|
|
||||||
#[serde(rename = "testimony")]
|
|
||||||
Testimony,
|
|
||||||
#[serde(rename = "holiday")]
|
|
||||||
Holiday,
|
|
||||||
#[serde(rename = "communion")]
|
|
||||||
Communion,
|
|
||||||
#[serde(rename = "baptism")]
|
|
||||||
Baptism,
|
|
||||||
#[serde(rename = "wedding")]
|
|
||||||
Wedding,
|
|
||||||
#[serde(rename = "funeral")]
|
|
||||||
Funeral,
|
|
||||||
#[serde(rename = "other")]
|
|
||||||
Other,
|
|
||||||
#[serde(rename = "livestream_archive")]
|
|
||||||
LivestreamArchive,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SermonSearch {
|
|
||||||
pub query: Option<String>,
|
|
||||||
pub speaker: Option<String>,
|
|
||||||
pub category: Option<SermonCategory>,
|
|
||||||
pub series: Option<String>,
|
|
||||||
pub date_from: Option<DateTime<Utc>>,
|
|
||||||
pub date_to: Option<DateTime<Utc>>,
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
pub featured_only: Option<bool>,
|
|
||||||
pub has_video: Option<bool>,
|
|
||||||
pub has_audio: Option<bool>,
|
|
||||||
pub has_transcript: Option<bool>,
|
|
||||||
pub min_duration: Option<u32>,
|
|
||||||
pub max_duration: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SermonSearch {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
query: None,
|
|
||||||
speaker: None,
|
|
||||||
category: None,
|
|
||||||
series: None,
|
|
||||||
date_from: None,
|
|
||||||
date_to: None,
|
|
||||||
tags: None,
|
|
||||||
featured_only: None,
|
|
||||||
has_video: None,
|
|
||||||
has_audio: None,
|
|
||||||
has_transcript: None,
|
|
||||||
min_duration: None,
|
|
||||||
max_duration: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SermonSearch {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_query(mut self, query: String) -> Self {
|
|
||||||
self.query = Some(query);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_speaker(mut self, speaker: String) -> Self {
|
|
||||||
self.speaker = Some(speaker);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_category(mut self, category: SermonCategory) -> Self {
|
|
||||||
self.category = Some(category);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_series(mut self, series: String) -> Self {
|
|
||||||
self.series = Some(series);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_date_range(mut self, from: DateTime<Utc>, to: DateTime<Utc>) -> Self {
|
|
||||||
self.date_from = Some(from);
|
|
||||||
self.date_to = Some(to);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn featured_only(mut self) -> Self {
|
|
||||||
self.featured_only = Some(true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_video(mut self) -> Self {
|
|
||||||
self.has_video = Some(true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_audio(mut self) -> Self {
|
|
||||||
self.has_audio = Some(true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_transcript(mut self) -> Self {
|
|
||||||
self.has_transcript = Some(true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sermon {
|
|
||||||
pub fn duration_formatted(&self) -> String {
|
|
||||||
self.duration_string.clone().unwrap_or_else(|| "Unknown".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_media(&self) -> bool {
|
|
||||||
self.media_url.is_some() || self.audio_url.is_some() || self.video_url.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_video(&self) -> bool {
|
|
||||||
self.video_url.is_some() || self.media_url.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_audio(&self) -> bool {
|
|
||||||
self.audio_url.is_some() || self.media_url.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_transcript(&self) -> bool {
|
|
||||||
self.transcript.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_recent(&self) -> bool {
|
|
||||||
let thirty_days_ago = Utc::now() - chrono::Duration::days(30);
|
|
||||||
self.date > thirty_days_ago
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_popular(&self) -> bool {
|
|
||||||
self.view_count > 100 || self.download_count > 50
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_tags(&self) -> Vec<String> {
|
|
||||||
self.tags.clone().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches_search(&self, search: &SermonSearch) -> bool {
|
|
||||||
if let Some(query) = &search.query {
|
|
||||||
let query_lower = query.to_lowercase();
|
|
||||||
if !self.title.to_lowercase().contains(&query_lower)
|
|
||||||
&& !self.description.to_lowercase().contains(&query_lower)
|
|
||||||
&& !self.speaker.to_lowercase().contains(&query_lower)
|
|
||||||
&& !self.scripture_reference.to_lowercase().contains(&query_lower) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(speaker) = &search.speaker {
|
|
||||||
if !self.speaker.to_lowercase().contains(&speaker.to_lowercase()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(category) = &search.category {
|
|
||||||
if self.category != *category {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(series) = &search.series {
|
|
||||||
match &self.series {
|
|
||||||
Some(sermon_series) => {
|
|
||||||
if !sermon_series.to_lowercase().contains(&series.to_lowercase()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(date_from) = search.date_from {
|
|
||||||
if self.date < date_from {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(date_to) = search.date_to {
|
|
||||||
if self.date > date_to {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(true) = search.featured_only {
|
|
||||||
if !self.is_featured {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(true) = search.has_video {
|
|
||||||
if !self.has_video() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(true) = search.has_audio {
|
|
||||||
if !self.has_audio() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(true) = search.has_transcript {
|
|
||||||
if !self.has_transcript() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SermonSeries {
|
|
||||||
pub fn is_active(&self) -> bool {
|
|
||||||
self.is_active && self.end_date.map_or(true, |end| end > Utc::now())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sermon_count(&self) -> usize {
|
|
||||||
self.sermons.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn total_duration(&self) -> Option<u32> {
|
|
||||||
if self.sermons.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we now use duration_string, we can't easily sum durations
|
|
||||||
// Return None for now - this would need proper duration parsing if needed
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn latest_sermon(&self) -> Option<&Sermon> {
|
|
||||||
self.sermons
|
|
||||||
.iter()
|
|
||||||
.max_by_key(|s| s.date)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn duration_formatted(&self) -> String {
|
|
||||||
match self.total_duration() {
|
|
||||||
Some(seconds) => {
|
|
||||||
let minutes = seconds / 60;
|
|
||||||
let hours = minutes / 60;
|
|
||||||
let remaining_minutes = minutes % 60;
|
|
||||||
|
|
||||||
if hours > 0 {
|
|
||||||
format!("{}h {}m", hours, remaining_minutes)
|
|
||||||
} else {
|
|
||||||
format!("{}m", minutes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => "Unknown".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// Device streaming capabilities
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum StreamingCapability {
|
|
||||||
/// Device supports AV1 codec (direct stream)
|
|
||||||
AV1,
|
|
||||||
/// Device needs HLS H.264 fallback
|
|
||||||
HLS,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streaming URL configuration
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct StreamingUrl {
|
|
||||||
pub url: String,
|
|
||||||
pub capability: StreamingCapability,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Device capability detection
|
|
||||||
pub struct DeviceCapabilities;
|
|
||||||
|
|
||||||
impl DeviceCapabilities {
|
|
||||||
/// Detect device streaming capability
|
|
||||||
/// For now, this is a simple implementation that can be expanded
|
|
||||||
#[cfg(target_os = "ios")]
|
|
||||||
pub fn detect_capability() -> StreamingCapability {
|
|
||||||
// Use sysctlbyname to get device model on iOS
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let name = CString::new("hw.model").unwrap();
|
|
||||||
let mut size: libc::size_t = 0;
|
|
||||||
|
|
||||||
// First call to get the size
|
|
||||||
if libc::sysctlbyname(
|
|
||||||
name.as_ptr(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
&mut size,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
0,
|
|
||||||
) != 0 {
|
|
||||||
println!("🎬 DEBUG: Failed to get model size, defaulting to HLS");
|
|
||||||
return StreamingCapability::HLS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate buffer and get the actual value
|
|
||||||
let mut buffer = vec![0u8; size];
|
|
||||||
if libc::sysctlbyname(
|
|
||||||
name.as_ptr(),
|
|
||||||
buffer.as_mut_ptr() as *mut libc::c_void,
|
|
||||||
&mut size,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
0,
|
|
||||||
) != 0 {
|
|
||||||
println!("🎬 DEBUG: Failed to get model value, defaulting to HLS");
|
|
||||||
return StreamingCapability::HLS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to string
|
|
||||||
if let Ok(model_cstr) = CStr::from_bytes_with_nul(&buffer[..size]) {
|
|
||||||
if let Ok(model) = model_cstr.to_str() {
|
|
||||||
let model = model.to_lowercase();
|
|
||||||
println!("🎬 DEBUG: Detected device model: {}", model);
|
|
||||||
|
|
||||||
// iPhone models with AV1 hardware decoding support:
|
|
||||||
// Marketing names: iPhone16,x = iPhone 15 Pro/Pro Max, iPhone17,x = iPhone 16 series
|
|
||||||
// Internal codenames: d9xap = iPhone 16 series, d8xap = iPhone 15 Pro series
|
|
||||||
if model.starts_with("iphone16,") || model.starts_with("iphone17,") ||
|
|
||||||
model.starts_with("d94ap") || model.starts_with("d93ap") ||
|
|
||||||
model.starts_with("d84ap") || model.starts_with("d83ap") {
|
|
||||||
println!("🎬 DEBUG: Device {} supports AV1 hardware decoding", model);
|
|
||||||
return StreamingCapability::AV1;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("🎬 DEBUG: Device {} does not support AV1, using HLS fallback", model);
|
|
||||||
return StreamingCapability::HLS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("🎬 DEBUG: Failed to parse model string, defaulting to HLS");
|
|
||||||
StreamingCapability::HLS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
pub fn detect_capability() -> StreamingCapability {
|
|
||||||
// Default to HLS for other platforms for now
|
|
||||||
StreamingCapability::HLS
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate streaming URL based on capability and media ID
|
|
||||||
pub fn get_streaming_url(base_url: &str, media_id: &str, capability: StreamingCapability) -> StreamingUrl {
|
|
||||||
// Add timestamp for cache busting to ensure fresh streams
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
let url = match capability {
|
|
||||||
StreamingCapability::AV1 => {
|
|
||||||
format!("{}/api/media/stream/{}?t={}", base_url.trim_end_matches('/'), media_id, timestamp)
|
|
||||||
}
|
|
||||||
StreamingCapability::HLS => {
|
|
||||||
format!("{}/api/media/stream/{}/playlist.m3u8?t={}", base_url.trim_end_matches('/'), media_id, timestamp)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
StreamingUrl { url, capability }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get optimal streaming URL for current device
|
|
||||||
pub fn get_optimal_streaming_url(base_url: &str, media_id: &str) -> StreamingUrl {
|
|
||||||
let capability = Self::detect_capability();
|
|
||||||
Self::get_streaming_url(base_url, media_id, capability)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_av1_url_generation() {
|
|
||||||
let url = DeviceCapabilities::get_streaming_url(
|
|
||||||
"https://api.rockvilletollandsda.church",
|
|
||||||
"test-id-123",
|
|
||||||
StreamingCapability::AV1
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(url.url, "https://api.rockvilletollandsda.church/api/media/stream/test-id-123");
|
|
||||||
assert_eq!(url.capability, StreamingCapability::AV1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_hls_url_generation() {
|
|
||||||
let url = DeviceCapabilities::get_streaming_url(
|
|
||||||
"https://api.rockvilletollandsda.church",
|
|
||||||
"test-id-123",
|
|
||||||
StreamingCapability::HLS
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(url.url, "https://api.rockvilletollandsda.church/api/media/stream/test-id-123/playlist.m3u8");
|
|
||||||
assert_eq!(url.capability, StreamingCapability::HLS);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_base_url_trimming() {
|
|
||||||
let url = DeviceCapabilities::get_streaming_url(
|
|
||||||
"https://api.rockvilletollandsda.church/",
|
|
||||||
"test-id-123",
|
|
||||||
StreamingCapability::HLS
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(url.url, "https://api.rockvilletollandsda.church/api/media/stream/test-id-123/playlist.m3u8");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
/// API version enum to specify which API version to use
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ApiVersion {
|
|
||||||
V1,
|
|
||||||
V2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiVersion {
|
|
||||||
pub fn path_prefix(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ApiVersion::V1 => "",
|
|
||||||
ApiVersion::V2 => "v2/",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,310 +0,0 @@
|
||||||
use crate::models::{ClientEvent, Sermon, Bulletin, BibleVerse};
|
|
||||||
use chrono::{DateTime, Utc, NaiveDateTime};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct FeedItem {
|
|
||||||
pub id: String,
|
|
||||||
pub feed_type: FeedItemType,
|
|
||||||
pub timestamp: String, // ISO8601 format
|
|
||||||
pub priority: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum FeedItemType {
|
|
||||||
#[serde(rename = "event")]
|
|
||||||
Event {
|
|
||||||
event: ClientEvent,
|
|
||||||
},
|
|
||||||
#[serde(rename = "sermon")]
|
|
||||||
Sermon {
|
|
||||||
sermon: Sermon,
|
|
||||||
},
|
|
||||||
#[serde(rename = "bulletin")]
|
|
||||||
Bulletin {
|
|
||||||
bulletin: Bulletin,
|
|
||||||
},
|
|
||||||
#[serde(rename = "verse")]
|
|
||||||
Verse {
|
|
||||||
verse: BibleVerse,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse date string to DateTime<Utc>, with fallback to current time
|
|
||||||
fn parse_date_with_fallback(date_str: &str) -> DateTime<Utc> {
|
|
||||||
// Try ISO8601 format first
|
|
||||||
if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
|
|
||||||
return dt.with_timezone(&Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try naive datetime parsing
|
|
||||||
if let Ok(naive) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S") {
|
|
||||||
return DateTime::from_naive_utc_and_offset(naive, Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to current time
|
|
||||||
Utc::now()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate priority for feed items based on type and recency
|
|
||||||
fn calculate_priority(feed_type: &FeedItemType, timestamp: &DateTime<Utc>) -> i32 {
|
|
||||||
let now = Utc::now();
|
|
||||||
let age_days = (now - *timestamp).num_days().max(0);
|
|
||||||
|
|
||||||
match feed_type {
|
|
||||||
FeedItemType::Event { .. } => {
|
|
||||||
// Events get highest priority, especially upcoming ones
|
|
||||||
if *timestamp > now {
|
|
||||||
1000 // Future events (upcoming)
|
|
||||||
} else {
|
|
||||||
800 - (age_days as i32) // Recent past events
|
|
||||||
}
|
|
||||||
},
|
|
||||||
FeedItemType::Sermon { .. } => {
|
|
||||||
// Sermons get high priority when recent
|
|
||||||
600 - (age_days as i32)
|
|
||||||
},
|
|
||||||
FeedItemType::Bulletin { .. } => {
|
|
||||||
// Bulletins get medium priority
|
|
||||||
400 - (age_days as i32)
|
|
||||||
},
|
|
||||||
FeedItemType::Verse { .. } => {
|
|
||||||
// Daily verse always gets consistent priority
|
|
||||||
300
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Aggregate and sort home feed items
|
|
||||||
pub fn aggregate_home_feed(
|
|
||||||
events: &[ClientEvent],
|
|
||||||
sermons: &[Sermon],
|
|
||||||
bulletins: &[Bulletin],
|
|
||||||
daily_verse: Option<&BibleVerse>
|
|
||||||
) -> Vec<FeedItem> {
|
|
||||||
let mut feed_items = Vec::new();
|
|
||||||
|
|
||||||
// Add recent sermons (limit to 3)
|
|
||||||
for sermon in sermons.iter().take(3) {
|
|
||||||
let timestamp = sermon.date; // Already a DateTime<Utc>
|
|
||||||
let feed_type = FeedItemType::Sermon { sermon: sermon.clone() };
|
|
||||||
let priority = calculate_priority(&feed_type, ×tamp);
|
|
||||||
|
|
||||||
feed_items.push(FeedItem {
|
|
||||||
id: format!("sermon_{}", sermon.id),
|
|
||||||
feed_type,
|
|
||||||
timestamp: timestamp.to_rfc3339(),
|
|
||||||
priority,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add upcoming events (limit to 2)
|
|
||||||
for event in events.iter().take(2) {
|
|
||||||
let timestamp = parse_date_with_fallback(&event.created_at);
|
|
||||||
let feed_type = FeedItemType::Event { event: event.clone() };
|
|
||||||
let priority = calculate_priority(&feed_type, ×tamp);
|
|
||||||
|
|
||||||
feed_items.push(FeedItem {
|
|
||||||
id: format!("event_{}", event.id),
|
|
||||||
feed_type,
|
|
||||||
timestamp: timestamp.to_rfc3339(),
|
|
||||||
priority,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add most recent bulletin
|
|
||||||
if let Some(bulletin) = bulletins.first() {
|
|
||||||
let timestamp = parse_date_with_fallback(&bulletin.date.to_string());
|
|
||||||
let feed_type = FeedItemType::Bulletin { bulletin: bulletin.clone() };
|
|
||||||
let priority = calculate_priority(&feed_type, ×tamp);
|
|
||||||
|
|
||||||
feed_items.push(FeedItem {
|
|
||||||
id: format!("bulletin_{}", bulletin.id),
|
|
||||||
feed_type,
|
|
||||||
timestamp: timestamp.to_rfc3339(),
|
|
||||||
priority,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add daily verse
|
|
||||||
if let Some(verse) = daily_verse {
|
|
||||||
let timestamp = Utc::now();
|
|
||||||
let feed_type = FeedItemType::Verse { verse: verse.clone() };
|
|
||||||
let priority = calculate_priority(&feed_type, ×tamp);
|
|
||||||
|
|
||||||
feed_items.push(FeedItem {
|
|
||||||
id: format!("verse_{}", verse.reference),
|
|
||||||
feed_type,
|
|
||||||
timestamp: timestamp.to_rfc3339(),
|
|
||||||
priority,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by priority (highest first), then by timestamp (newest first)
|
|
||||||
feed_items.sort_by(|a, b| {
|
|
||||||
b.priority.cmp(&a.priority)
|
|
||||||
.then_with(|| b.timestamp.cmp(&a.timestamp))
|
|
||||||
});
|
|
||||||
|
|
||||||
feed_items
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Media type enumeration for content categorization
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum MediaType {
|
|
||||||
Sermons,
|
|
||||||
LiveStreams,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MediaType {
|
|
||||||
pub fn display_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
MediaType::Sermons => "Sermons",
|
|
||||||
MediaType::LiveStreams => "Live Archives",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
MediaType::Sermons => "play.rectangle.fill",
|
|
||||||
MediaType::LiveStreams => "dot.radiowaves.left.and.right",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get sermons or livestreams based on media type
|
|
||||||
pub fn get_media_content(sermons: &[Sermon], media_type: &MediaType) -> Vec<Sermon> {
|
|
||||||
match media_type {
|
|
||||||
MediaType::Sermons => {
|
|
||||||
// Filter for regular sermons (non-livestream)
|
|
||||||
sermons.iter()
|
|
||||||
.filter(|sermon| !sermon.title.to_lowercase().contains("livestream"))
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
MediaType::LiveStreams => {
|
|
||||||
// Filter for livestream archives
|
|
||||||
sermons.iter()
|
|
||||||
.filter(|sermon| sermon.title.to_lowercase().contains("livestream"))
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::models::{ClientEvent, Sermon, Bulletin, BibleVerse};
|
|
||||||
|
|
||||||
fn create_sample_event(id: &str, title: &str) -> ClientEvent {
|
|
||||||
ClientEvent {
|
|
||||||
id: id.to_string(),
|
|
||||||
title: title.to_string(),
|
|
||||||
description: "Sample description".to_string(),
|
|
||||||
date: "2025-01-15".to_string(),
|
|
||||||
start_time: "6:00 PM".to_string(),
|
|
||||||
end_time: "8:00 PM".to_string(),
|
|
||||||
location: "Sample Location".to_string(),
|
|
||||||
location_url: None,
|
|
||||||
image: None,
|
|
||||||
thumbnail: None,
|
|
||||||
category: "Social".to_string(),
|
|
||||||
is_featured: false,
|
|
||||||
recurring_type: None,
|
|
||||||
tags: None,
|
|
||||||
contact_email: None,
|
|
||||||
contact_phone: None,
|
|
||||||
registration_url: None,
|
|
||||||
max_attendees: None,
|
|
||||||
current_attendees: None,
|
|
||||||
created_at: "2025-01-10T10:00:00Z".to_string(),
|
|
||||||
updated_at: "2025-01-10T10:00:00Z".to_string(),
|
|
||||||
duration_minutes: 120,
|
|
||||||
has_registration: false,
|
|
||||||
is_full: false,
|
|
||||||
spots_remaining: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_sample_sermon(id: &str, title: &str) -> Sermon {
|
|
||||||
Sermon {
|
|
||||||
id: id.to_string(),
|
|
||||||
title: title.to_string(),
|
|
||||||
description: Some("Sample sermon".to_string()),
|
|
||||||
date: Some("2025-01-10T10:00:00Z".to_string()),
|
|
||||||
video_url: Some("https://example.com/video".to_string()),
|
|
||||||
audio_url: None,
|
|
||||||
thumbnail_url: None,
|
|
||||||
duration: None,
|
|
||||||
speaker: Some("Pastor Smith".to_string()),
|
|
||||||
series: None,
|
|
||||||
scripture_references: None,
|
|
||||||
tags: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_aggregate_home_feed() {
|
|
||||||
let events = vec![
|
|
||||||
create_sample_event("1", "Event 1"),
|
|
||||||
create_sample_event("2", "Event 2"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let sermons = vec![
|
|
||||||
create_sample_sermon("1", "Sermon 1"),
|
|
||||||
create_sample_sermon("2", "Sermon 2"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let bulletins = vec![
|
|
||||||
Bulletin {
|
|
||||||
id: "1".to_string(),
|
|
||||||
title: "Weekly Bulletin".to_string(),
|
|
||||||
date: "2025-01-12T10:00:00Z".to_string(),
|
|
||||||
pdf_url: "https://example.com/bulletin.pdf".to_string(),
|
|
||||||
description: Some("This week's bulletin".to_string()),
|
|
||||||
thumbnail_url: None,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let verse = BibleVerse {
|
|
||||||
text: "For God so loved the world...".to_string(),
|
|
||||||
reference: "John 3:16".to_string(),
|
|
||||||
version: Some("KJV".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let feed = aggregate_home_feed(&events, &sermons, &bulletins, Some(&verse));
|
|
||||||
|
|
||||||
assert!(feed.len() >= 4); // Should have events, sermons, bulletin, and verse
|
|
||||||
|
|
||||||
// Check that items are sorted by priority
|
|
||||||
for i in 1..feed.len() {
|
|
||||||
assert!(feed[i-1].priority >= feed[i].priority);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_media_type_display() {
|
|
||||||
assert_eq!(MediaType::Sermons.display_name(), "Sermons");
|
|
||||||
assert_eq!(MediaType::LiveStreams.display_name(), "Live Archives");
|
|
||||||
assert_eq!(MediaType::Sermons.icon_name(), "play.rectangle.fill");
|
|
||||||
assert_eq!(MediaType::LiveStreams.icon_name(), "dot.radiowaves.left.and.right");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_media_content() {
|
|
||||||
let sermons = vec![
|
|
||||||
create_sample_sermon("1", "Regular Sermon"),
|
|
||||||
create_sample_sermon("2", "Livestream Service"),
|
|
||||||
create_sample_sermon("3", "Another Sermon"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let regular_sermons = get_media_content(&sermons, &MediaType::Sermons);
|
|
||||||
assert_eq!(regular_sermons.len(), 2);
|
|
||||||
|
|
||||||
let livestreams = get_media_content(&sermons, &MediaType::LiveStreams);
|
|
||||||
assert_eq!(livestreams.len(), 1);
|
|
||||||
assert!(livestreams[0].title.contains("Livestream"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
use crate::models::ClientEvent;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct FormattedEvent {
|
|
||||||
pub formatted_time: String,
|
|
||||||
pub formatted_date_time: String,
|
|
||||||
pub is_multi_day: bool,
|
|
||||||
pub formatted_date_range: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format time range for display
|
|
||||||
pub fn format_time_range(start_time: &str, end_time: &str) -> String {
|
|
||||||
format!("{} - {}", start_time, end_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if event appears to be multi-day based on date format
|
|
||||||
pub fn is_multi_day_event(date: &str) -> bool {
|
|
||||||
date.contains(" - ")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format date and time for display, handling multi-day events
|
|
||||||
pub fn format_date_time(date: &str, start_time: &str, end_time: &str) -> String {
|
|
||||||
if is_multi_day_event(date) {
|
|
||||||
// For multi-day events, integrate times with their respective dates
|
|
||||||
let components: Vec<&str> = date.split(" - ").collect();
|
|
||||||
if components.len() == 2 {
|
|
||||||
format!("{} at {} - {} at {}", components[0], start_time, components[1], end_time)
|
|
||||||
} else {
|
|
||||||
date.to_string() // Fallback to original date
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Single day events: return just the date (time displayed separately)
|
|
||||||
date.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a client event with all display formatting logic
|
|
||||||
pub fn format_event_for_display(event: &ClientEvent) -> FormattedEvent {
|
|
||||||
let start_time = &event.start_time;
|
|
||||||
let end_time = &event.end_time;
|
|
||||||
|
|
||||||
// Derive formatted date from start_time since ClientEvent no longer has date field
|
|
||||||
let formatted_date = format_date_from_timestamp(start_time);
|
|
||||||
|
|
||||||
FormattedEvent {
|
|
||||||
formatted_time: format_time_range(start_time, end_time),
|
|
||||||
formatted_date_time: format_date_time(&formatted_date, start_time, end_time),
|
|
||||||
is_multi_day: is_multi_day_event(&formatted_date),
|
|
||||||
formatted_date_range: formatted_date,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract formatted date from ISO timestamp
|
|
||||||
fn format_date_from_timestamp(timestamp: &str) -> String {
|
|
||||||
use chrono::{DateTime, FixedOffset};
|
|
||||||
|
|
||||||
if let Ok(dt) = DateTime::parse_from_str(timestamp, "%Y-%m-%dT%H:%M:%S%z") {
|
|
||||||
dt.format("%A, %B %d, %Y").to_string()
|
|
||||||
} else {
|
|
||||||
"Date TBD".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format duration in minutes to human readable format
|
|
||||||
pub fn format_duration_minutes(minutes: i64) -> String {
|
|
||||||
if minutes < 60 {
|
|
||||||
format!("{} min", minutes)
|
|
||||||
} else {
|
|
||||||
let hours = minutes / 60;
|
|
||||||
let remaining_minutes = minutes % 60;
|
|
||||||
if remaining_minutes == 0 {
|
|
||||||
format!("{} hr", hours)
|
|
||||||
} else {
|
|
||||||
format!("{} hr {} min", hours, remaining_minutes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format spots remaining for events
|
|
||||||
pub fn format_spots_remaining(current: Option<u32>, max: Option<u32>) -> Option<String> {
|
|
||||||
match (current, max) {
|
|
||||||
(Some(current), Some(max)) => {
|
|
||||||
let remaining = max.saturating_sub(current);
|
|
||||||
if remaining == 0 {
|
|
||||||
Some("Event Full".to_string())
|
|
||||||
} else {
|
|
||||||
Some(format!("{} spots remaining", remaining))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if event registration is full
|
|
||||||
pub fn is_event_full(current: Option<u32>, max: Option<u32>) -> bool {
|
|
||||||
match (current, max) {
|
|
||||||
(Some(current), Some(max)) => current >= max,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_time_range() {
|
|
||||||
assert_eq!(format_time_range("9:00 AM", "5:00 PM"), "9:00 AM - 5:00 PM");
|
|
||||||
assert_eq!(format_time_range("", ""), " - ");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_multi_day_event() {
|
|
||||||
assert!(is_multi_day_event("Saturday, Aug 30, 2025 - Sunday, Aug 31, 2025"));
|
|
||||||
assert!(!is_multi_day_event("Saturday, Aug 30, 2025"));
|
|
||||||
assert!(!is_multi_day_event(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_date_time() {
|
|
||||||
// Single day event
|
|
||||||
let result = format_date_time("Saturday, Aug 30, 2025", "6:00 PM", "8:00 PM");
|
|
||||||
assert_eq!(result, "Saturday, Aug 30, 2025");
|
|
||||||
|
|
||||||
// Multi-day event
|
|
||||||
let result = format_date_time(
|
|
||||||
"Saturday, Aug 30, 2025 - Sunday, Aug 31, 2025",
|
|
||||||
"6:00 PM",
|
|
||||||
"6:00 AM"
|
|
||||||
);
|
|
||||||
assert_eq!(result, "Saturday, Aug 30, 2025 at 6:00 PM - Sunday, Aug 31, 2025 at 6:00 AM");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_duration_minutes() {
|
|
||||||
assert_eq!(format_duration_minutes(30), "30 min");
|
|
||||||
assert_eq!(format_duration_minutes(60), "1 hr");
|
|
||||||
assert_eq!(format_duration_minutes(90), "1 hr 30 min");
|
|
||||||
assert_eq!(format_duration_minutes(120), "2 hr");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_spots_remaining() {
|
|
||||||
assert_eq!(format_spots_remaining(Some(8), Some(10)), Some("2 spots remaining".to_string()));
|
|
||||||
assert_eq!(format_spots_remaining(Some(10), Some(10)), Some("Event Full".to_string()));
|
|
||||||
assert_eq!(format_spots_remaining(None, Some(10)), None);
|
|
||||||
assert_eq!(format_spots_remaining(Some(5), None), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_event_full() {
|
|
||||||
assert!(is_event_full(Some(10), Some(10)));
|
|
||||||
assert!(is_event_full(Some(11), Some(10))); // Over capacity
|
|
||||||
assert!(!is_event_full(Some(9), Some(10)));
|
|
||||||
assert!(!is_event_full(None, Some(10)));
|
|
||||||
assert!(!is_event_full(Some(5), None));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
pub mod scripture;
|
|
||||||
pub mod validation;
|
|
||||||
pub mod formatting;
|
|
||||||
pub mod feed;
|
|
||||||
|
|
||||||
pub use scripture::*;
|
|
||||||
pub use validation::*;
|
|
||||||
pub use formatting::*;
|
|
||||||
pub use feed::*;
|
|
|
@ -1,164 +0,0 @@
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ScriptureSection {
|
|
||||||
pub verse: String,
|
|
||||||
pub reference: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format raw scripture text into structured sections with verses and references
|
|
||||||
pub fn format_scripture_text(text: &str) -> Vec<ScriptureSection> {
|
|
||||||
// Handle single-line format where verse and reference are together
|
|
||||||
if text.contains(" KJV") && !text.contains('\n') {
|
|
||||||
// Single line format: "verse text. Book chapter:verse KJV"
|
|
||||||
if let Some(kjv_pos) = text.rfind(" KJV") {
|
|
||||||
let before_kjv = &text[..kjv_pos];
|
|
||||||
// Find the last period or other punctuation that separates verse from reference
|
|
||||||
if let Some(last_period) = before_kjv.rfind('.') {
|
|
||||||
if let Some(reference_start) = before_kjv[last_period..].find(char::is_alphabetic) {
|
|
||||||
let actual_start = last_period + reference_start;
|
|
||||||
let verse_text = format!("{}.", &before_kjv[..last_period]);
|
|
||||||
let reference = format!("{} KJV", &before_kjv[actual_start..]);
|
|
||||||
return vec![ScriptureSection {
|
|
||||||
verse: verse_text.trim().to_string(),
|
|
||||||
reference: reference.trim().to_string(),
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: treat entire text as verse with no separate reference
|
|
||||||
return vec![ScriptureSection {
|
|
||||||
verse: text.to_string(),
|
|
||||||
reference: String::new(),
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-line format (original logic)
|
|
||||||
let sections: Vec<&str> = text.split('\n').collect();
|
|
||||||
let mut formatted_sections = Vec::new();
|
|
||||||
let mut current_verse = String::new();
|
|
||||||
|
|
||||||
for section in sections {
|
|
||||||
let trimmed = section.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this line is a reference (contains "KJV" at the end)
|
|
||||||
if trimmed.ends_with("KJV") {
|
|
||||||
// This is a reference for the verse we just accumulated
|
|
||||||
if !current_verse.is_empty() {
|
|
||||||
formatted_sections.push(ScriptureSection {
|
|
||||||
verse: current_verse.clone(),
|
|
||||||
reference: trimmed.to_string(),
|
|
||||||
});
|
|
||||||
current_verse.clear(); // Reset for next verse
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This is verse text
|
|
||||||
if !current_verse.is_empty() {
|
|
||||||
current_verse.push(' ');
|
|
||||||
}
|
|
||||||
current_verse.push_str(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any remaining verse without a reference
|
|
||||||
if !current_verse.is_empty() {
|
|
||||||
formatted_sections.push(ScriptureSection {
|
|
||||||
verse: current_verse,
|
|
||||||
reference: String::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted_sections
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract scripture references from text (e.g., "Joel 2:28 KJV" patterns)
|
|
||||||
pub fn extract_scripture_references(text: &str) -> String {
|
|
||||||
let pattern = r"([1-3]?\s*[A-Za-z]+\s+\d+:\d+(?:-\d+)?)\s+KJV";
|
|
||||||
|
|
||||||
match Regex::new(pattern) {
|
|
||||||
Ok(regex) => {
|
|
||||||
let references: Vec<String> = regex
|
|
||||||
.captures_iter(text)
|
|
||||||
.filter_map(|cap| cap.get(1).map(|m| m.as_str().trim().to_string()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if references.is_empty() {
|
|
||||||
"Scripture Reading".to_string()
|
|
||||||
} else {
|
|
||||||
references.join(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => "Scripture Reading".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create standardized share text for sermons
|
|
||||||
pub fn create_sermon_share_text(title: &str, speaker: &str, video_url: Option<&str>, audio_url: Option<&str>) -> Vec<String> {
|
|
||||||
let mut items = Vec::new();
|
|
||||||
|
|
||||||
// Create share text
|
|
||||||
let share_text = format!("Check out this sermon: \"{}\" by {}", title, speaker);
|
|
||||||
items.push(share_text);
|
|
||||||
|
|
||||||
// Add video URL if available, otherwise audio URL
|
|
||||||
if let Some(url) = video_url {
|
|
||||||
items.push(url.to_string());
|
|
||||||
} else if let Some(url) = audio_url {
|
|
||||||
items.push(url.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_single_line_scripture_format() {
|
|
||||||
let input = "And it shall come to pass afterward, that I will pour out my spirit upon all flesh. Joel 2:28 KJV";
|
|
||||||
let result = format_scripture_text(input);
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
|
||||||
assert_eq!(result[0].verse, "And it shall come to pass afterward, that I will pour out my spirit upon all flesh.");
|
|
||||||
assert_eq!(result[0].reference, "Joel 2:28 KJV");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multi_line_scripture_format() {
|
|
||||||
let input = "And it shall come to pass afterward, that I will pour out my spirit upon all flesh\nJoel 2:28 KJV\nQuench not the Spirit. Despise not prophesyings.\n1 Thessalonians 5:19-21 KJV";
|
|
||||||
let result = format_scripture_text(input);
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 2);
|
|
||||||
assert_eq!(result[0].verse, "And it shall come to pass afterward, that I will pour out my spirit upon all flesh");
|
|
||||||
assert_eq!(result[0].reference, "Joel 2:28 KJV");
|
|
||||||
assert_eq!(result[1].verse, "Quench not the Spirit. Despise not prophesyings.");
|
|
||||||
assert_eq!(result[1].reference, "1 Thessalonians 5:19-21 KJV");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_scripture_references() {
|
|
||||||
let input = "Some text with Joel 2:28 KJV and 1 Thessalonians 5:19-21 KJV references";
|
|
||||||
let result = extract_scripture_references(input);
|
|
||||||
|
|
||||||
assert_eq!(result, "Joel 2:28, 1 Thessalonians 5:19-21");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_sermon_share_text() {
|
|
||||||
let result = create_sermon_share_text(
|
|
||||||
"Test Sermon",
|
|
||||||
"John Doe",
|
|
||||||
Some("https://example.com/video"),
|
|
||||||
Some("https://example.com/audio")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(result.len(), 2);
|
|
||||||
assert_eq!(result[0], "Check out this sermon: \"Test Sermon\" by John Doe");
|
|
||||||
assert_eq!(result[1], "https://example.com/video");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Utc, NaiveDateTime};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ValidationResult {
|
|
||||||
pub is_valid: bool,
|
|
||||||
pub errors: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValidationResult {
|
|
||||||
pub fn valid() -> Self {
|
|
||||||
Self {
|
|
||||||
is_valid: true,
|
|
||||||
errors: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn invalid(errors: Vec<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
is_valid: false,
|
|
||||||
errors,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_error(&mut self, error: String) {
|
|
||||||
self.errors.push(error);
|
|
||||||
self.is_valid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ContactFormData {
|
|
||||||
pub name: String,
|
|
||||||
pub email: String,
|
|
||||||
pub phone: String,
|
|
||||||
pub message: String,
|
|
||||||
pub subject: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate email address using regex
|
|
||||||
pub fn is_valid_email(email: &str) -> bool {
|
|
||||||
let email_regex = Regex::new(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$").unwrap();
|
|
||||||
email_regex.is_match(email)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate phone number - must be exactly 10 digits
|
|
||||||
pub fn is_valid_phone(phone: &str) -> bool {
|
|
||||||
let digits_only: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
|
|
||||||
digits_only.len() == 10
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate contact form with all business rules
|
|
||||||
pub fn validate_contact_form(form_data: &ContactFormData) -> ValidationResult {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
let trimmed_name = form_data.name.trim();
|
|
||||||
let trimmed_email = form_data.email.trim();
|
|
||||||
let trimmed_phone = form_data.phone.trim();
|
|
||||||
let trimmed_message = form_data.message.trim();
|
|
||||||
|
|
||||||
// Name validation
|
|
||||||
if trimmed_name.is_empty() || trimmed_name.len() < 2 {
|
|
||||||
errors.push("Name must be at least 2 characters".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email validation
|
|
||||||
if trimmed_email.is_empty() {
|
|
||||||
errors.push("Email is required".to_string());
|
|
||||||
} else if !is_valid_email(trimmed_email) {
|
|
||||||
errors.push("Please enter a valid email address".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phone validation (optional, but if provided must be valid)
|
|
||||||
if !trimmed_phone.is_empty() && !is_valid_phone(trimmed_phone) {
|
|
||||||
errors.push("Please enter a valid phone number".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message validation
|
|
||||||
if trimmed_message.is_empty() {
|
|
||||||
errors.push("Message is required".to_string());
|
|
||||||
} else if trimmed_message.len() < 10 {
|
|
||||||
errors.push("Message must be at least 10 characters".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
ValidationResult::valid()
|
|
||||||
} else {
|
|
||||||
ValidationResult::invalid(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanitize and trim form input
|
|
||||||
pub fn sanitize_form_input(input: &str) -> String {
|
|
||||||
input.trim().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse date from various frontend formats
|
|
||||||
/// Supports: "2025-06-28T23:00", "2025-06-28 23:00", and RFC3339 formats
|
|
||||||
pub fn parse_datetime_flexible(date_str: &str) -> Option<DateTime<Utc>> {
|
|
||||||
let trimmed = date_str.trim();
|
|
||||||
|
|
||||||
// First try RFC3339/ISO 8601 with timezone info
|
|
||||||
if trimmed.contains('Z') || trimmed.contains('+') || trimmed.contains('-') && trimmed.rfind('-').map_or(false, |i| i > 10) {
|
|
||||||
if let Ok(dt) = DateTime::parse_from_rfc3339(trimmed) {
|
|
||||||
return Some(dt.with_timezone(&Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try ISO 8601 with 'Z' suffix
|
|
||||||
if let Ok(dt) = chrono::DateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S%.3fZ") {
|
|
||||||
return Some(dt.with_timezone(&Utc));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try ISO 8601 without milliseconds but with Z
|
|
||||||
if let Ok(dt) = chrono::DateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%SZ") {
|
|
||||||
return Some(dt.with_timezone(&Utc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try local datetime formats (no timezone info) - treat as UTC
|
|
||||||
let local_formats = [
|
|
||||||
"%Y-%m-%dT%H:%M:%S%.3f", // ISO 8601 with milliseconds, no timezone
|
|
||||||
"%Y-%m-%dT%H:%M:%S", // ISO 8601 no milliseconds, no timezone
|
|
||||||
"%Y-%m-%dT%H:%M", // ISO 8601 no seconds, no timezone (frontend format)
|
|
||||||
"%Y-%m-%d %H:%M:%S", // Space separated with seconds
|
|
||||||
"%Y-%m-%d %H:%M", // Space separated no seconds
|
|
||||||
"%m/%d/%Y %H:%M:%S", // US format with seconds
|
|
||||||
"%m/%d/%Y %H:%M", // US format no seconds
|
|
||||||
];
|
|
||||||
|
|
||||||
for format in &local_formats {
|
|
||||||
if let Ok(naive_dt) = NaiveDateTime::parse_from_str(trimmed, format) {
|
|
||||||
return Some(DateTime::from_naive_utc_and_offset(naive_dt, Utc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try date-only formats (no time) - set time to midnight UTC
|
|
||||||
let date_formats = [
|
|
||||||
"%Y-%m-%d", // ISO date
|
|
||||||
"%m/%d/%Y", // US date
|
|
||||||
"%d/%m/%Y", // European date
|
|
||||||
];
|
|
||||||
|
|
||||||
for format in &date_formats {
|
|
||||||
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(trimmed, format) {
|
|
||||||
return Some(DateTime::from_naive_utc_and_offset(
|
|
||||||
naive_date.and_hms_opt(0, 0, 0).unwrap(),
|
|
||||||
Utc,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate datetime string can be parsed
|
|
||||||
pub fn is_valid_datetime(datetime_str: &str) -> bool {
|
|
||||||
parse_datetime_flexible(datetime_str).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_valid_email() {
|
|
||||||
assert!(is_valid_email("test@example.com"));
|
|
||||||
assert!(is_valid_email("user.name+tag@domain.co.uk"));
|
|
||||||
assert!(!is_valid_email("invalid.email"));
|
|
||||||
assert!(!is_valid_email("@domain.com"));
|
|
||||||
assert!(!is_valid_email("user@"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_valid_phone() {
|
|
||||||
assert!(is_valid_phone("1234567890"));
|
|
||||||
assert!(is_valid_phone("(123) 456-7890"));
|
|
||||||
assert!(is_valid_phone("123-456-7890"));
|
|
||||||
assert!(!is_valid_phone("12345"));
|
|
||||||
assert!(!is_valid_phone("12345678901"));
|
|
||||||
assert!(!is_valid_phone("abc1234567"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_contact_form_validation() {
|
|
||||||
let valid_form = ContactFormData {
|
|
||||||
name: "John Doe".to_string(),
|
|
||||||
email: "john@example.com".to_string(),
|
|
||||||
phone: "1234567890".to_string(),
|
|
||||||
message: "This is a test message with enough characters.".to_string(),
|
|
||||||
subject: "Test Subject".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = validate_contact_form(&valid_form);
|
|
||||||
assert!(result.is_valid);
|
|
||||||
assert!(result.errors.is_empty());
|
|
||||||
|
|
||||||
let invalid_form = ContactFormData {
|
|
||||||
name: "A".to_string(), // Too short
|
|
||||||
email: "invalid-email".to_string(), // Invalid email
|
|
||||||
phone: "123".to_string(), // Invalid phone
|
|
||||||
message: "Short".to_string(), // Too short message
|
|
||||||
subject: "".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = validate_contact_form(&invalid_form);
|
|
||||||
assert!(!result.is_valid);
|
|
||||||
assert_eq!(result.errors.len(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_datetime_flexible() {
|
|
||||||
// Test frontend format (the main case we're solving)
|
|
||||||
assert!(parse_datetime_flexible("2025-06-28T23:00").is_some());
|
|
||||||
|
|
||||||
// Test RFC3339 with Z
|
|
||||||
assert!(parse_datetime_flexible("2024-01-15T14:30:00Z").is_some());
|
|
||||||
|
|
||||||
// Test RFC3339 with timezone offset
|
|
||||||
assert!(parse_datetime_flexible("2024-01-15T14:30:00-05:00").is_some());
|
|
||||||
|
|
||||||
// Test ISO 8601 without timezone (should work as local time)
|
|
||||||
assert!(parse_datetime_flexible("2024-01-15T14:30:00").is_some());
|
|
||||||
|
|
||||||
// Test with milliseconds
|
|
||||||
assert!(parse_datetime_flexible("2024-01-15T14:30:00.000Z").is_some());
|
|
||||||
|
|
||||||
// Test space separated
|
|
||||||
assert!(parse_datetime_flexible("2024-01-15 14:30:00").is_some());
|
|
||||||
|
|
||||||
// Test date only
|
|
||||||
assert!(parse_datetime_flexible("2024-01-15").is_some());
|
|
||||||
|
|
||||||
// Test US format
|
|
||||||
assert!(parse_datetime_flexible("01/15/2024 14:30").is_some());
|
|
||||||
|
|
||||||
// Test invalid format
|
|
||||||
assert!(parse_datetime_flexible("invalid-date").is_none());
|
|
||||||
assert!(parse_datetime_flexible("").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_valid_datetime() {
|
|
||||||
assert!(is_valid_datetime("2025-06-28T23:00"));
|
|
||||||
assert!(is_valid_datetime("2024-01-15T14:30:00Z"));
|
|
||||||
assert!(!is_valid_datetime("invalid-date"));
|
|
||||||
assert!(!is_valid_datetime(""));
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
@ -30,6 +30,7 @@ export const {
|
||||||
fetchCurrentBulletinJson,
|
fetchCurrentBulletinJson,
|
||||||
fetchBibleVerseJson,
|
fetchBibleVerseJson,
|
||||||
submitEventJson,
|
submitEventJson,
|
||||||
|
submitEventWithImageJson,
|
||||||
// Admin functions
|
// Admin functions
|
||||||
fetchAllSchedulesJson,
|
fetchAllSchedulesJson,
|
||||||
createScheduleJson,
|
createScheduleJson,
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue