Complete sermon itinerary application with Nuxt 3, SQLite, authentication, and Docker deployment
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
data/*.db
|
||||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Database
|
||||||
|
data/
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create data directory for SQLite database
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NUXT_HOST=0.0.0.0
|
||||||
|
ENV NUXT_PORT=3000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", ".output/server/index.mjs"]
|
||||||
167
README.md
Normal file
167
README.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# New Life Christian Church - Sermon Itinerary
|
||||||
|
|
||||||
|
A web application for managing and displaying weekly sermons for New Life Christian Church.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📝 **Sermon Management**: Create and manage sermon content with a user-friendly form
|
||||||
|
- 🔐 **Authentication**: Secure admin access with login system
|
||||||
|
- 📱 **QR Codes**: Generate QR codes for easy sermon sharing
|
||||||
|
- 📅 **Date-based URLs**: Sermons accessible via `sermon-MMDDYYYY` format
|
||||||
|
- 🎨 **Modern UI**: Clean, responsive design using Tailwind CSS and Inter font
|
||||||
|
- 📊 **Three Sections**: Bible References, Personal Appliance, and Pastor's Challenge
|
||||||
|
- 🗂️ **Smart Organization**: Recent sermons (last 3 months) displayed by default, older sermons in dropdown
|
||||||
|
- 🐳 **Docker Ready**: Fully containerized for easy deployment
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Frontend**: Nuxt 3 (Vue.js)
|
||||||
|
- **Styling**: Tailwind CSS with Inter font from Google Fonts
|
||||||
|
- **Database**: SQLite
|
||||||
|
- **QR Codes**: qrcode library
|
||||||
|
- **Deployment**: Docker & Docker Compose
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed on your system
|
||||||
|
|
||||||
|
### Installation & Deployment
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd nlcc-itinerary
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update environment variables in `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- AUTH_SECRET=your-secret-key-change-in-production
|
||||||
|
- SITE_URL=https://newlife-christian.com
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build and run with Docker Compose:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`
|
||||||
|
|
||||||
|
### Default Credentials
|
||||||
|
|
||||||
|
- **Username**: admin
|
||||||
|
- **Password**: admin123
|
||||||
|
|
||||||
|
⚠️ **Important**: Change these credentials in production by modifying `server/utils/database.ts`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
nlcc-itinerary/
|
||||||
|
├── assets/css/ # Global styles
|
||||||
|
├── components/ # Vue components
|
||||||
|
│ ├── SermonCard.vue
|
||||||
|
│ ├── QRCodeButton.vue
|
||||||
|
│ └── QRCodeModal.vue
|
||||||
|
├── middleware/ # Route middleware
|
||||||
|
│ └── auth.ts
|
||||||
|
├── pages/ # Application pages
|
||||||
|
│ ├── index.vue # Main sermon listing
|
||||||
|
│ ├── login.vue # Admin login
|
||||||
|
│ ├── admin.vue # Sermon creation form
|
||||||
|
│ └── [slug].vue # Individual sermon page
|
||||||
|
├── server/
|
||||||
|
│ ├── api/ # API endpoints
|
||||||
|
│ │ ├── auth/ # Authentication endpoints
|
||||||
|
│ │ └── sermons/ # Sermon CRUD endpoints
|
||||||
|
│ └── utils/ # Server utilities
|
||||||
|
│ ├── database.ts # SQLite database functions
|
||||||
|
│ └── auth.ts # Authentication helpers
|
||||||
|
├── logos/ # Church logos
|
||||||
|
├── Dockerfile # Docker configuration
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
└── nuxt.config.ts # Nuxt configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a Sermon
|
||||||
|
|
||||||
|
1. Navigate to `/login` and sign in with admin credentials
|
||||||
|
2. You'll be redirected to `/admin`
|
||||||
|
3. Fill in the sermon details:
|
||||||
|
- **Date**: Select the sermon date (URL will be auto-generated as `sermon-MMDDYYYY`)
|
||||||
|
- **Title**: Enter the sermon title
|
||||||
|
- **Bible References**: Add one or more Bible verses (use +/- buttons)
|
||||||
|
- **Personal Appliance**: Enter personal application content
|
||||||
|
- **Pastor's Challenge**: Enter the pastor's challenge content
|
||||||
|
4. Click "Create Sermon"
|
||||||
|
|
||||||
|
### Viewing Sermons
|
||||||
|
|
||||||
|
- **Main Page** (`/`): Shows recent sermons (last 3 months) with option to view older ones
|
||||||
|
- **Individual Sermon** (`/sermon-MMDDYYYY`): Full sermon details with QR code
|
||||||
|
- **QR Code**: Click the QR code button on any sermon card or page to generate a scannable code
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
The application uses SQLite with the following schema:
|
||||||
|
|
||||||
|
### Sermons Table
|
||||||
|
- `id`: Primary key
|
||||||
|
- `slug`: Unique sermon identifier (e.g., sermon-09282025)
|
||||||
|
- `title`: Sermon title
|
||||||
|
- `date`: Sermon date
|
||||||
|
- `bible_references`: Newline-separated Bible verses
|
||||||
|
- `personal_appliance`: Personal application content
|
||||||
|
- `pastors_challenge`: Pastor's challenge content
|
||||||
|
- `created_at`: Timestamp
|
||||||
|
|
||||||
|
### Users Table
|
||||||
|
- `id`: Primary key
|
||||||
|
- `username`: User's username
|
||||||
|
- `password`: User's password (plain text - should be hashed in production)
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
⚠️ **For Production Use**:
|
||||||
|
|
||||||
|
1. Change the default admin credentials
|
||||||
|
2. Implement proper password hashing (bcrypt, argon2, etc.)
|
||||||
|
3. Use a strong `AUTH_SECRET` in environment variables
|
||||||
|
4. Enable HTTPS
|
||||||
|
5. Consider implementing rate limiting
|
||||||
|
6. Add CSRF protection
|
||||||
|
|
||||||
|
## Docker Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop containers
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Rebuild after changes
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Access container shell
|
||||||
|
docker exec -it nlcc-itinerary sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
The SQLite database is stored in the `./data` directory, which is mounted as a volume in Docker. This ensures sermon data persists across container restarts.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is created for New Life Christian Church.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, please contact the development team.
|
||||||
11
assets/css/main.css
Normal file
11
assets/css/main.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-gray-50;
|
||||||
|
}
|
||||||
35
components/QRCodeButton.vue
Normal file
35
components/QRCodeButton.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="showModal = true"
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
||||||
|
</svg>
|
||||||
|
QR Code
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<QRCodeModal
|
||||||
|
v-if="showModal"
|
||||||
|
:sermon="sermon"
|
||||||
|
@close="showModal = false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Sermon {
|
||||||
|
id?: number
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
bible_references: string
|
||||||
|
personal_appliance: string
|
||||||
|
pastors_challenge: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
sermon: Sermon
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
|
</script>
|
||||||
63
components/QRCodeModal.vue
Normal file
63
components/QRCodeModal.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" @click.self="$emit('close')">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">Scan to View Sermon</h3>
|
||||||
|
<button
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="bg-white p-4 rounded-lg border-2 border-gray-200">
|
||||||
|
<canvas ref="qrCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-gray-600 text-center">{{ sermon.title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import QRCode from 'qrcode'
|
||||||
|
|
||||||
|
interface Sermon {
|
||||||
|
id?: number
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
bible_references: string
|
||||||
|
personal_appliance: string
|
||||||
|
pastors_challenge: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sermon: Sermon
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const qrCanvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (qrCanvas.value) {
|
||||||
|
const url = `${config.public.siteUrl}/${props.sermon.slug}`
|
||||||
|
await QRCode.toCanvas(qrCanvas.value, url, {
|
||||||
|
width: 256,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#FFFFFF'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
44
components/SermonCard.vue
Normal file
44
components/SermonCard.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow overflow-hidden">
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{ sermon.title }}</h3>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">{{ formatDate(sermon.date) }}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/${sermon.slug}`"
|
||||||
|
class="text-blue-600 hover:text-blue-700 font-medium text-sm"
|
||||||
|
>
|
||||||
|
View Sermon →
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<QRCodeButton :sermon="sermon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Sermon {
|
||||||
|
id?: number
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
bible_references: string
|
||||||
|
personal_appliance: string
|
||||||
|
pastors_challenge: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
sermon: Sermon
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function formatDate(dateString: string) {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
nlcc-itinerary:
|
||||||
|
build: .
|
||||||
|
container_name: nlcc-itinerary
|
||||||
|
ports:
|
||||||
|
- "3002:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- AUTH_SECRET=d8c7c1735fc853b807c1bccce791b054
|
||||||
|
- SITE_URL=https://nlcc.rydertech.us
|
||||||
|
restart: unless-stopped
|
||||||
9
middleware/auth.ts
Normal file
9
middleware/auth.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
if (import.meta.server) return
|
||||||
|
|
||||||
|
const { data } = await useFetch('/api/auth/verify')
|
||||||
|
|
||||||
|
if (!data.value?.authenticated) {
|
||||||
|
return navigateTo('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
36
nuxt.config.ts
Normal file
36
nuxt.config.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-04-03',
|
||||||
|
devtools: { enabled: true },
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@nuxt/ui'
|
||||||
|
],
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
title: 'New Life Christian Church - Sermons',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ name: 'description', content: 'Weekly sermons from New Life Christian Church' }
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', type: 'image/png', href: '/logos/favicon.png' },
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
||||||
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
css: ['~/assets/css/main.css'],
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
authSecret: process.env.AUTH_SECRET || 'change-this-secret-in-production',
|
||||||
|
public: {
|
||||||
|
siteUrl: process.env.SITE_URL || 'https://newlife-christian.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "nlcc-itinerary",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/ui": "^2.18.4",
|
||||||
|
"better-sqlite3": "^11.3.0",
|
||||||
|
"nuxt": "^3.13.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"vue": "^3.5.5",
|
||||||
|
"vue-router": "^4.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/tailwindcss": "^6.12.1",
|
||||||
|
"@types/better-sqlite3": "^7.6.11",
|
||||||
|
"@types/qrcode": "^1.5.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
106
pages/[slug].vue
Normal file
106
pages/[slug].vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<header class="bg-white shadow-sm">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<NuxtLink to="/" class="flex items-center space-x-4 hover:opacity-80">
|
||||||
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||||
|
</NuxtLink>
|
||||||
|
<QRCodeButton v-if="sermon" :sermon="sermon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div v-if="sermon" class="bg-white shadow-lg rounded-lg p-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="border-b pb-6 mb-8">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 mb-2">{{ sermon.title }}</h1>
|
||||||
|
<p class="text-lg text-gray-600">{{ formatDate(sermon.date) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 1: Bible References -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Bible References</h2>
|
||||||
|
<div class="bg-blue-50 rounded-lg p-6">
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li
|
||||||
|
v-for="(ref, index) in bibleReferences"
|
||||||
|
:key="index"
|
||||||
|
class="text-gray-800 flex items-start"
|
||||||
|
>
|
||||||
|
<span class="text-blue-600 mr-2">•</span>
|
||||||
|
<span>{{ ref }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 2: Personal Appliance -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Personal Appliance</h2>
|
||||||
|
<div class="bg-green-50 rounded-lg p-6">
|
||||||
|
<p class="text-gray-800 whitespace-pre-wrap">{{ sermon.personal_appliance }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 3: Pastor's Challenge -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Pastor's Challenge</h2>
|
||||||
|
<div class="bg-purple-50 rounded-lg p-6">
|
||||||
|
<p class="text-gray-800 whitespace-pre-wrap">{{ sermon.pastors_challenge }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Back Button -->
|
||||||
|
<div class="border-t pt-6">
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Back to All Sermons
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else class="bg-white shadow-lg rounded-lg p-8 text-center">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Sermon Not Found</h2>
|
||||||
|
<p class="text-gray-600 mb-6">The sermon you're looking for doesn't exist.</p>
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Back to All Sermons
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const slug = route.params.slug as string
|
||||||
|
|
||||||
|
const { data: sermon } = await useFetch(`/api/sermons/${slug}`)
|
||||||
|
|
||||||
|
const bibleReferences = computed(() => {
|
||||||
|
if (!sermon.value?.bible_references) return []
|
||||||
|
return sermon.value.bible_references.split('\n').filter((ref: string) => ref.trim())
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatDate(dateString: string) {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
212
pages/admin.vue
Normal file
212
pages/admin.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<header class="bg-white shadow-sm">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Create New Sermon</h1>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="handleLogout"
|
||||||
|
class="text-sm font-medium text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<form @submit.prevent="handleSubmit" class="bg-white shadow-lg rounded-lg p-8 space-y-8">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label for="date" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Sermon Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="date"
|
||||||
|
v-model="formData.date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Sermon Title
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="formData.title"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Enter sermon title"
|
||||||
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 1: Bible References -->
|
||||||
|
<div class="border-t pt-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Section 1: Bible References</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="(ref, index) in bibleReferences" :key="index" class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="bibleReferences[index]"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., John 3:16"
|
||||||
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="bibleReferences.length > 1"
|
||||||
|
type="button"
|
||||||
|
@click="removeReference(index)"
|
||||||
|
class="px-3 py-2 bg-red-100 text-red-700 rounded-md hover:bg-red-200"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="addReference"
|
||||||
|
class="px-4 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 font-medium"
|
||||||
|
>
|
||||||
|
+ Add Reference
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 2: Personal Appliance -->
|
||||||
|
<div class="border-t pt-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Section 2: Personal Appliance</h2>
|
||||||
|
<textarea
|
||||||
|
v-model="formData.personal_appliance"
|
||||||
|
rows="6"
|
||||||
|
required
|
||||||
|
placeholder="Enter personal appliance content..."
|
||||||
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 3: Pastor's Challenge -->
|
||||||
|
<div class="border-t pt-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Section 3: Pastor's Challenge</h2>
|
||||||
|
<textarea
|
||||||
|
v-model="formData.pastors_challenge"
|
||||||
|
rows="6"
|
||||||
|
required
|
||||||
|
placeholder="Enter pastor's challenge content..."
|
||||||
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error/Success Messages -->
|
||||||
|
<div v-if="error" class="text-red-600 text-sm">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div v-if="success" class="text-green-600 text-sm">
|
||||||
|
{{ success }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="flex-1 py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ loading ? 'Creating...' : 'Create Sermon' }}
|
||||||
|
</button>
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="px-6 py-3 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth'
|
||||||
|
})
|
||||||
|
|
||||||
|
const bibleReferences = ref([''])
|
||||||
|
const formData = ref({
|
||||||
|
date: '',
|
||||||
|
title: '',
|
||||||
|
personal_appliance: '',
|
||||||
|
pastors_challenge: ''
|
||||||
|
})
|
||||||
|
const error = ref('')
|
||||||
|
const success = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
function addReference() {
|
||||||
|
bibleReferences.value.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeReference(index: number) {
|
||||||
|
bibleReferences.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateToSlug(date: string) {
|
||||||
|
// Convert YYYY-MM-DD to MMDDYYYY
|
||||||
|
const [year, month, day] = date.split('-')
|
||||||
|
return `sermon-${month}${day}${year}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
error.value = ''
|
||||||
|
success.value = ''
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const slug = formatDateToSlug(formData.value.date)
|
||||||
|
const bible_references = bibleReferences.value.filter(ref => ref.trim()).join('\n')
|
||||||
|
|
||||||
|
await $fetch('/api/sermons', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
slug,
|
||||||
|
title: formData.value.title,
|
||||||
|
date: formData.value.date,
|
||||||
|
bible_references,
|
||||||
|
personal_appliance: formData.value.personal_appliance,
|
||||||
|
pastors_challenge: formData.value.pastors_challenge
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
success.value = 'Sermon created successfully!'
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
formData.value = {
|
||||||
|
date: '',
|
||||||
|
title: '',
|
||||||
|
personal_appliance: '',
|
||||||
|
pastors_challenge: ''
|
||||||
|
}
|
||||||
|
bibleReferences.value = ['']
|
||||||
|
|
||||||
|
// Redirect after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateTo('/')
|
||||||
|
}, 2000)
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.data?.message || 'Failed to create sermon'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
99
pages/index.vue
Normal file
99
pages/index.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="bg-white shadow-sm">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||||
|
</div>
|
||||||
|
<NuxtLink
|
||||||
|
to="/login"
|
||||||
|
class="text-sm font-medium text-blue-600 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
Admin Login
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">
|
||||||
|
Welcome to New Life! Please choose the sermon you'd like to see.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<!-- Recent Sermons (Last 3 months) -->
|
||||||
|
<div v-if="recentSermons.length > 0">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Recent Sermons</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||||
|
<SermonCard
|
||||||
|
v-for="sermon in recentSermons"
|
||||||
|
:key="sermon.id"
|
||||||
|
:sermon="sermon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Older Sermons Dropdown -->
|
||||||
|
<div v-if="olderSermons.length > 0" class="mt-8">
|
||||||
|
<button
|
||||||
|
@click="showOlder = !showOlder"
|
||||||
|
class="w-full bg-white border border-gray-300 rounded-lg px-6 py-4 text-left hover:bg-gray-50 transition-colors flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-medium text-gray-900">View Older Sermons</span>
|
||||||
|
<svg
|
||||||
|
:class="['w-5 h-5 text-gray-500 transition-transform', showOlder ? 'rotate-180' : '']"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="showOlder" class="mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<SermonCard
|
||||||
|
v-for="sermon in olderSermons"
|
||||||
|
:key="sermon.id"
|
||||||
|
:sermon="sermon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="recentSermons.length === 0 && olderSermons.length === 0" class="text-center py-12">
|
||||||
|
<p class="text-gray-500 text-lg">No sermons available yet.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const showOlder = ref(false)
|
||||||
|
|
||||||
|
// Fetch all sermons
|
||||||
|
const { data: allSermons } = await useFetch('/api/sermons')
|
||||||
|
|
||||||
|
// Calculate date 3 months ago
|
||||||
|
const threeMonthsAgo = new Date()
|
||||||
|
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
|
||||||
|
|
||||||
|
// Split sermons into recent and older
|
||||||
|
const recentSermons = computed(() => {
|
||||||
|
if (!allSermons.value) return []
|
||||||
|
return allSermons.value.filter((sermon: any) => {
|
||||||
|
const sermonDate = new Date(sermon.date)
|
||||||
|
return sermonDate >= threeMonthsAgo
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const olderSermons = computed(() => {
|
||||||
|
if (!allSermons.value) return []
|
||||||
|
return allSermons.value.filter((sermon: any) => {
|
||||||
|
const sermonDate = new Date(sermon.date)
|
||||||
|
return sermonDate < threeMonthsAgo
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
93
pages/login.vue
Normal file
93
pages/login.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 flex items-center justify-center px-4">
|
||||||
|
<div class="max-w-md w-full">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-20 w-auto mx-auto mb-4" />
|
||||||
|
<h2 class="text-3xl font-bold text-gray-900">Admin Login</h2>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">Sign in to manage sermons</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white py-8 px-6 shadow-lg rounded-lg">
|
||||||
|
<form @submit.prevent="handleLogin" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-700">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="text-red-600 text-sm">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ loading ? 'Signing in...' : 'Sign in' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<NuxtLink to="/" class="text-sm text-blue-600 hover:text-blue-700">
|
||||||
|
← Back to Sermons
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center text-sm text-gray-600">
|
||||||
|
<p>Default credentials: admin / admin123</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
error.value = ''
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
username: username.value,
|
||||||
|
password: password.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
await navigateTo('/admin')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.data?.message || 'Invalid credentials'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
30
server/api/auth/login.post.ts
Normal file
30
server/api/auth/login.post.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { getUserByUsername } from '~/server/utils/database'
|
||||||
|
import { setAuthCookie } from '~/server/utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { username, password } = body
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Username and password are required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserByUsername(username)
|
||||||
|
|
||||||
|
if (!user || user.password !== password) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Invalid credentials'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthCookie(event, username)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
})
|
||||||
9
server/api/auth/logout.post.ts
Normal file
9
server/api/auth/logout.post.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { clearAuthCookie } from '~/server/utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
clearAuthCookie(event)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
})
|
||||||
9
server/api/auth/verify.get.ts
Normal file
9
server/api/auth/verify.get.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { isAuthenticated } from '~/server/utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const authenticated = isAuthenticated(event)
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated
|
||||||
|
}
|
||||||
|
})
|
||||||
23
server/api/sermons/[slug].get.ts
Normal file
23
server/api/sermons/[slug].get.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { getSermonBySlug } from '~/server/utils/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const slug = getRouterParam(event, 'slug')
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Slug is required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sermon = getSermonBySlug(slug)
|
||||||
|
|
||||||
|
if (!sermon) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'Sermon not found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sermon
|
||||||
|
})
|
||||||
10
server/api/sermons/index.get.ts
Normal file
10
server/api/sermons/index.get.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { getAllSermons } from '~/server/utils/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const query = getQuery(event)
|
||||||
|
const limit = query.limit ? parseInt(query.limit as string) : undefined
|
||||||
|
|
||||||
|
const sermons = getAllSermons(limit)
|
||||||
|
|
||||||
|
return sermons
|
||||||
|
})
|
||||||
43
server/api/sermons/index.post.ts
Normal file
43
server/api/sermons/index.post.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { createSermon } from '~/server/utils/database'
|
||||||
|
import { isAuthenticated } from '~/server/utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Check authentication
|
||||||
|
if (!isAuthenticated(event)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { slug, title, date, bible_references, personal_appliance, pastors_challenge } = body
|
||||||
|
|
||||||
|
if (!slug || !title || !date || !bible_references || !personal_appliance || !pastors_challenge) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'All fields are required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
createSermon({
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
bible_references,
|
||||||
|
personal_appliance,
|
||||||
|
pastors_challenge
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Sermon created successfully'
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: error.message || 'Failed to create sermon'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
23
server/utils/auth.ts
Normal file
23
server/utils/auth.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { H3Event } from 'h3'
|
||||||
|
|
||||||
|
export function setAuthCookie(event: H3Event, username: string) {
|
||||||
|
setCookie(event, 'auth', username, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
path: '/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthCookie(event: H3Event) {
|
||||||
|
return getCookie(event, 'auth')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAuthCookie(event: H3Event) {
|
||||||
|
deleteCookie(event, 'auth')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAuthenticated(event: H3Event): boolean {
|
||||||
|
const auth = getAuthCookie(event)
|
||||||
|
return !!auth
|
||||||
|
}
|
||||||
93
server/utils/database.ts
Normal file
93
server/utils/database.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import Database from 'better-sqlite3'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
let db: Database.Database | null = null
|
||||||
|
|
||||||
|
export interface Sermon {
|
||||||
|
id?: number
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
bible_references: string
|
||||||
|
personal_appliance: string
|
||||||
|
pastors_challenge: string
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id?: number
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDatabase() {
|
||||||
|
if (!db) {
|
||||||
|
const dbPath = join(process.cwd(), 'data', 'sermons.db')
|
||||||
|
db = new Database(dbPath)
|
||||||
|
|
||||||
|
// Create tables if they don't exist
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS sermons (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
slug TEXT UNIQUE NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
bible_references TEXT NOT NULL,
|
||||||
|
personal_appliance TEXT NOT NULL,
|
||||||
|
pastors_challenge TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Insert default admin user (password: admin123)
|
||||||
|
// In production, this should be hashed properly
|
||||||
|
const userExists = db.prepare('SELECT COUNT(*) as count FROM users WHERE username = ?').get('admin') as { count: number }
|
||||||
|
if (userExists.count === 0) {
|
||||||
|
db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run('admin', 'admin123')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllSermons(limit?: number) {
|
||||||
|
const db = getDatabase()
|
||||||
|
if (limit) {
|
||||||
|
return db.prepare('SELECT * FROM sermons ORDER BY date DESC LIMIT ?').all(limit) as Sermon[]
|
||||||
|
}
|
||||||
|
return db.prepare('SELECT * FROM sermons ORDER BY date DESC').all() as Sermon[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSermonBySlug(slug: string) {
|
||||||
|
const db = getDatabase()
|
||||||
|
return db.prepare('SELECT * FROM sermons WHERE slug = ?').get(slug) as Sermon | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSermon(sermon: Sermon) {
|
||||||
|
const db = getDatabase()
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO sermons (slug, title, date, bible_references, personal_appliance, pastors_challenge)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`)
|
||||||
|
return stmt.run(
|
||||||
|
sermon.slug,
|
||||||
|
sermon.title,
|
||||||
|
sermon.date,
|
||||||
|
sermon.bible_references,
|
||||||
|
sermon.personal_appliance,
|
||||||
|
sermon.pastors_challenge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserByUsername(username: string) {
|
||||||
|
const db = getDatabase()
|
||||||
|
return db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined
|
||||||
|
}
|
||||||
18
tailwind.config.js
Normal file
18
tailwind.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./components/**/*.{js,vue,ts}",
|
||||||
|
"./layouts/**/*.vue",
|
||||||
|
"./pages/**/*.vue",
|
||||||
|
"./plugins/**/*.{js,ts}",
|
||||||
|
"./app.vue",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user