Complete sermon itinerary application with Nuxt 3, SQLite, authentication, and Docker deployment
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user