Compare commits
12 Commits
599b2f0685
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f11b36fa4 | |||
| ede88a7d7d | |||
| 57715f0e92 | |||
| 6269655ae1 | |||
| 1c2b80d837 | |||
| 8c66550987 | |||
| fc17db62d4 | |||
| 752e74b2ed | |||
| 1515fba6c9 | |||
| 0ff37e8999 | |||
| 3a50cbebdd | |||
| 3f1c573a67 |
@@ -1,12 +1,23 @@
|
||||
<template>
|
||||
<footer class="bg-gray-100 border-t border-gray-200 mt-auto">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
Created for New Life Christian Church by Joshua Ryder with AI assistance
|
||||
</p>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
Created for New Life Christian Church by Joshua Ryder with AI assistance
|
||||
</p>
|
||||
<button
|
||||
@click="showTermsModal = true"
|
||||
class="text-xs text-gray-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
Terms of Use & Privacy Policy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TermsModal v-model="showTermsModal" />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const showTermsModal = ref(false)
|
||||
</script>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="py-1">
|
||||
<!-- Home Link -->
|
||||
<NuxtLink
|
||||
v-if="showHome"
|
||||
v-if="showHome && !isOnHomePage"
|
||||
to="/"
|
||||
@click="isOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@@ -54,6 +54,7 @@
|
||||
<template v-if="isAuthenticated">
|
||||
<!-- Profile Link -->
|
||||
<NuxtLink
|
||||
v-if="!isOnProfilePage"
|
||||
to="/profile"
|
||||
@click="isOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@@ -64,6 +65,7 @@
|
||||
<!-- Admin Links -->
|
||||
<template v-if="isAdmin">
|
||||
<NuxtLink
|
||||
v-if="!isOnAdminPage"
|
||||
to="/admin"
|
||||
@click="isOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@@ -71,6 +73,7 @@
|
||||
📝 Manage Sermons
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-if="!isOnUsersPage"
|
||||
to="/users"
|
||||
@click="isOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
@@ -91,6 +94,7 @@
|
||||
<!-- Not authenticated -->
|
||||
<template v-else>
|
||||
<NuxtLink
|
||||
v-if="!isOnLoginPage"
|
||||
:to="loginRedirectUrl"
|
||||
@click="isOpen = false"
|
||||
class="block px-4 py-2 text-sm text-green-700 hover:bg-green-50 font-medium"
|
||||
@@ -98,6 +102,7 @@
|
||||
🔑 Log In
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-if="!isOnRegisterPage"
|
||||
to="/register"
|
||||
@click="isOpen = false"
|
||||
class="block px-4 py-2 text-sm text-blue-700 hover:bg-blue-50 font-medium"
|
||||
@@ -130,6 +135,20 @@ const loginRedirectUrl = computed(() => {
|
||||
return `/login?redirect=${encodeURIComponent(redirectPath)}`
|
||||
})
|
||||
|
||||
// Determine which page we're currently on to hide self-referencing links
|
||||
const currentPathNormalized = computed(() => {
|
||||
const path = props.currentPath || route.fullPath
|
||||
// Remove query params and trailing slashes for comparison
|
||||
return path.split('?')[0].replace(/\/$/, '')
|
||||
})
|
||||
|
||||
const isOnHomePage = computed(() => currentPathNormalized.value === '' || currentPathNormalized.value === '/')
|
||||
const isOnProfilePage = computed(() => currentPathNormalized.value === '/profile')
|
||||
const isOnAdminPage = computed(() => currentPathNormalized.value === '/admin')
|
||||
const isOnUsersPage = computed(() => currentPathNormalized.value === '/users')
|
||||
const isOnLoginPage = computed(() => currentPathNormalized.value === '/login')
|
||||
const isOnRegisterPage = computed(() => currentPathNormalized.value === '/register')
|
||||
|
||||
// Toggle menu with debounce to prevent accidental double-taps
|
||||
const toggleMenu = () => {
|
||||
if (isToggling.value) return
|
||||
|
||||
309
components/RichTextEditor.vue
Normal file
309
components/RichTextEditor.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div class="rich-text-editor border border-gray-300 rounded-lg shadow-sm overflow-hidden" :class="editorClass">
|
||||
<!-- Toolbar -->
|
||||
<div v-if="editor" class="toolbar bg-gray-50 border-b border-gray-300 p-2 flex flex-wrap gap-1">
|
||||
<!-- Undo/Redo -->
|
||||
<button
|
||||
@click="editor.chain().focus().undo().run()"
|
||||
:disabled="!editor.can().undo()"
|
||||
:class="toolbarButtonClass"
|
||||
title="Undo (Ctrl+Z)"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().redo().run()"
|
||||
:disabled="!editor.can().redo()"
|
||||
:class="toolbarButtonClass"
|
||||
title="Redo (Ctrl+Y)"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 mx-1"></div>
|
||||
|
||||
<!-- Text Formatting -->
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('bold') ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Bold (Ctrl+B)"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('italic') ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Italic (Ctrl+I)"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4h-8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('underline') ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Underline (Ctrl+U)"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('strike') ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Strikethrough"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 mx-1"></div>
|
||||
|
||||
<!-- Highlight -->
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHighlight().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('highlight') ? 'bg-yellow-100 text-yellow-700' : '']"
|
||||
title="Highlight"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17.75 7L14 3.25l-10 10V17h3.75l10-10zm2.96-2.96c.39-.39.39-1.02 0-1.41L18.37.29c-.39-.39-1.02-.39-1.41 0L15 2.25 18.75 6l1.96-1.96z"/>
|
||||
<path d="M0 20h24v4H0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 mx-1"></div>
|
||||
|
||||
<!-- Lists -->
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('bulletList') ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Bullet List"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('orderedList') ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Numbered List"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 mx-1"></div>
|
||||
|
||||
<!-- Headings -->
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('heading', { level: 2 }) ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Heading 2"
|
||||
type="button"
|
||||
>
|
||||
<span class="font-bold text-sm">H2</span>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
:class="[toolbarButtonClass, editor.isActive('heading', { level: 3 }) ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Heading 3"
|
||||
type="button"
|
||||
>
|
||||
<span class="font-bold text-sm">H3</span>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 mx-1"></div>
|
||||
|
||||
<!-- Alignment -->
|
||||
<button
|
||||
@click="editor.chain().focus().setTextAlign('left').run()"
|
||||
:class="[toolbarButtonClass, editor.isActive({ textAlign: 'left' }) ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Align Left"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().setTextAlign('center').run()"
|
||||
:class="[toolbarButtonClass, editor.isActive({ textAlign: 'center' }) ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Align Center"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().setTextAlign('right').run()"
|
||||
:class="[toolbarButtonClass, editor.isActive({ textAlign: 'right' }) ? 'bg-blue-100 text-blue-700' : '']"
|
||||
title="Align Right"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-gray-300 mx-1"></div>
|
||||
|
||||
<!-- Clear Formatting -->
|
||||
<button
|
||||
@click="editor.chain().focus().clearNodes().unsetAllMarks().run()"
|
||||
:class="toolbarButtonClass"
|
||||
title="Clear Formatting"
|
||||
type="button"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M6 5v.18L8.82 8h2.4l-.72 1.68 2.1 2.1L14.21 8H20V5H6zm14 14l-1.41-1.41-5.82-5.82L7.76 6.76 6.41 5.41 5 6.82l2.23 2.23-1.61 3.73L1 17.43V21h3.57l4.66-4.66 5.73 5.73L16.41 23 18 21.41z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor Content -->
|
||||
<EditorContent :editor="editor" class="prose max-w-none p-4 min-h-[16rem] focus:outline-none" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
editorClass?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const toolbarButtonClass = computed(() =>
|
||||
'p-2 rounded hover:bg-gray-200 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
|
||||
)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Highlight,
|
||||
Underline,
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
],
|
||||
content: props.modelValue,
|
||||
onUpdate: ({ editor }) => {
|
||||
emit('update:modelValue', editor.getHTML())
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose max-w-none focus:outline-none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Watch for external changes to modelValue
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (editor.value && newValue !== editor.value.getHTML()) {
|
||||
editor.value.commands.setContent(newValue, false)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Tiptap Editor Styles */
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 1rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.ProseMirror h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0.75rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.ProseMirror ul,
|
||||
.ProseMirror ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.ProseMirror ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror mark {
|
||||
background-color: #fef08a;
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.ProseMirror strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ProseMirror em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ProseMirror u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ProseMirror s {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
288
components/TermsModal.vue
Normal file
288
components/TermsModal.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<!-- Modal Overlay -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50"
|
||||
@click.self="closeModal"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between p-4 sm:p-6 border-b border-gray-200 sticky top-0 bg-white rounded-t-lg">
|
||||
<h2 class="text-xl sm:text-2xl font-bold text-gray-900">Terms of Use & Privacy Policy</h2>
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors p-1"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- Modal Body (Scrollable) -->
|
||||
<div class="overflow-y-auto p-4 sm:p-6 flex-1">
|
||||
<!-- AI Disclaimer Section -->
|
||||
<section class="mb-6 p-4 sm:p-6 bg-yellow-50 border-l-4 border-yellow-400 rounded">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3 flex items-center">
|
||||
<span class="mr-2">⚠️</span> Important: AI-Generated Content Disclaimer
|
||||
</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p class="font-semibold">
|
||||
This website and application were created with artificial intelligence (AI) assistance.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Please be aware:</strong> AI technology, while powerful, is not perfect and can make mistakes.
|
||||
This includes but is not limited to:
|
||||
</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Errors in code or functionality</li>
|
||||
<li>Unexpected behavior or bugs</li>
|
||||
<li>Security vulnerabilities</li>
|
||||
<li>Data processing or storage issues</li>
|
||||
<li>Incorrect information display</li>
|
||||
</ul>
|
||||
<p class="font-semibold text-red-700 mt-3">
|
||||
Users should not rely on this system as the sole repository for important information.
|
||||
Always maintain backup copies of your sermon notes and important data.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Terms of Use -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">Terms of Use</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p>
|
||||
By accessing and using this website (the "Service"), you accept and agree to be bound by the
|
||||
terms and provisions of this agreement.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">Purpose</h4>
|
||||
<p>
|
||||
This Service is provided by New Life Christian Church (the "Church") to assist members and
|
||||
attendees in viewing sermon information and taking personal notes during services. The Service
|
||||
is provided as-is for informational and organizational purposes only.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">Use at Your Own Risk</h4>
|
||||
<p>
|
||||
You acknowledge and agree that your use of this Service is at your sole risk. The Service
|
||||
is provided on an "AS IS" and "AS AVAILABLE" basis without warranties of any kind, either
|
||||
express or implied.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Limitation of Liability -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">Limitation of Liability</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p class="font-semibold">
|
||||
TO THE FULLEST EXTENT PERMITTED BY LAW, NEW LIFE CHRISTIAN CHURCH, ITS STAFF, VOLUNTEERS,
|
||||
CONTRACTORS, AND AFFILIATES SHALL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING FROM
|
||||
THE USE OF THIS SERVICE.
|
||||
</p>
|
||||
<p>This includes but is not limited to:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Loss of data, including sermon notes or personal information</li>
|
||||
<li>Service interruptions or downtime</li>
|
||||
<li>Errors, bugs, or technical malfunctions</li>
|
||||
<li>Security breaches or unauthorized access</li>
|
||||
<li>Inaccurate or incomplete information</li>
|
||||
<li>Any indirect, incidental, special, consequential, or punitive damages</li>
|
||||
</ul>
|
||||
<p class="mt-3">
|
||||
The Church makes no guarantee regarding the availability, reliability, or accuracy of the
|
||||
Service and reserves the right to modify, suspend, or discontinue the Service at any time
|
||||
without notice.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Data & Privacy -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">Privacy & Data</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">Information We Collect</h4>
|
||||
<p>When you register for an account, we collect:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Username and password (password is encrypted)</li>
|
||||
<li>Email address</li>
|
||||
<li>First and last name</li>
|
||||
<li>Your sermon notes and related content</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">How We Use Your Information</h4>
|
||||
<p>Your information is used to:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Provide access to the Service</li>
|
||||
<li>Store and manage your sermon notes</li>
|
||||
<li>Send you sermon notes via email when requested</li>
|
||||
<li>Authenticate your account</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">Data Retention</h4>
|
||||
<p>
|
||||
Your notes are stored in our database and are associated with your account.
|
||||
<strong class="text-red-700">Important:</strong> If a sermon is deleted by an administrator,
|
||||
all associated user notes for that sermon are permanently deleted. We recommend regularly
|
||||
downloading or emailing your notes to maintain personal backups.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">Data Security</h4>
|
||||
<p>
|
||||
While we implement security measures to protect your data, no system is completely secure.
|
||||
Given that this system was created with AI assistance, there may be unknown vulnerabilities.
|
||||
The Church cannot guarantee the security of your information.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base sm:text-lg font-semibold text-gray-900 mt-4 mb-2">Third-Party Services</h4>
|
||||
<p>
|
||||
When you email your notes, we use third-party email service providers to deliver your messages.
|
||||
Your email address and content are transmitted through these services.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- User Responsibilities -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">User Responsibilities</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p>As a user of this Service, you agree to:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-1">
|
||||
<li>Maintain the confidentiality of your account credentials</li>
|
||||
<li>Use the Service only for its intended purpose</li>
|
||||
<li>Maintain your own backups of important notes and information</li>
|
||||
<li>Not attempt to compromise the security of the Service</li>
|
||||
<li>Not use the Service for any unlawful purpose</li>
|
||||
<li>Accept sole responsibility for any content you create or upload</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Account Termination -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">Account Termination</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p>
|
||||
The Church reserves the right to terminate or suspend your account at any time, with or
|
||||
without notice, for any reason, including violation of these terms or at the Church's sole
|
||||
discretion.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Changes to Terms -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">Changes to These Terms</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p>
|
||||
The Church reserves the right to modify these terms at any time. Continued use of the
|
||||
Service after changes constitutes acceptance of the modified terms.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact -->
|
||||
<section class="mb-6">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 mb-3">Contact Information</h3>
|
||||
<div class="space-y-3 text-sm sm:text-base text-gray-700">
|
||||
<p>
|
||||
If you have questions about these terms or the Service, please contact New Life Christian
|
||||
Church through our regular church communication channels.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Acceptance -->
|
||||
<section class="mb-6 p-4 sm:p-6 bg-gray-50 border rounded">
|
||||
<p class="text-sm sm:text-base text-gray-700">
|
||||
<strong>By using this Service, you acknowledge that you have read, understood, and agree to
|
||||
be bound by these Terms of Use and Privacy Policy, including the AI-generated content disclaimer.</strong>
|
||||
</p>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mt-3">
|
||||
Last Updated: November 7, 2025
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="border-t border-gray-200 p-4 sm:p-6 bg-gray-50 rounded-b-lg">
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="w-full sm:w-auto px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const closeModal = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// Close on escape key
|
||||
onMounted(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && props.modelValue) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleEscape)
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleEscape)
|
||||
})
|
||||
})
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
watch(() => props.modelValue, (isOpen) => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active .bg-white,
|
||||
.modal-leave-active .bg-white {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from .bg-white,
|
||||
.modal-leave-to .bg-white {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
nlcc-itinerary:
|
||||
image: glcr.rydertech.us/ryder/nlcc-itinerary:latest
|
||||
image: git.rydertech.us/rydertech/nlcc-itinerary:latest
|
||||
container_name: nlcc-itinerary
|
||||
ports:
|
||||
- "3002:3000"
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/extension-highlight": "^2.10.3",
|
||||
"@tiptap/extension-text-align": "^2.10.3",
|
||||
"@tiptap/extension-underline": "^2.10.3",
|
||||
"@tiptap/starter-kit": "^2.10.3",
|
||||
"@tiptap/vue-3": "^2.10.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
@@ -24,6 +29,7 @@
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/qrcode": "^1.5.5"
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"patch-package": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +117,11 @@
|
||||
<p class="mb-3 text-sm text-red-600 font-medium">
|
||||
⚠️ Notes are deleted if a Sermon is deleted by an admin. Please email or download notes to have a copy sent to you.
|
||||
</p>
|
||||
<textarea
|
||||
<RichTextEditor
|
||||
v-model="notes"
|
||||
@input="handleNotesChange"
|
||||
placeholder="Take notes during the sermon..."
|
||||
class="w-full h-64 px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-y"
|
||||
:class="fontSizeClasses"
|
||||
></textarea>
|
||||
@update:modelValue="handleNotesChange"
|
||||
:editorClass="fontSizeClasses"
|
||||
/>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<p v-if="saveStatus" class="text-sm" :class="saveStatus === 'Saved' ? 'text-green-600' : 'text-gray-500'">
|
||||
{{ saveStatus }}
|
||||
@@ -137,13 +135,19 @@
|
||||
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed font-medium transition-colors"
|
||||
>
|
||||
<span v-if="emailStatus === 'sending'">Sending...</span>
|
||||
<span v-else>📧 Email Notes</span>
|
||||
<span v-else class="flex flex-col items-center">
|
||||
<span>📧 Email Notes</span>
|
||||
<span class="text-xs opacity-80 mt-0.5">(Formatting included)</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="downloadNotes"
|
||||
class="flex-1 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium transition-colors"
|
||||
>
|
||||
📥 Download Notes
|
||||
<span class="flex flex-col items-center">
|
||||
<span>📥 Download Notes</span>
|
||||
<span class="text-xs opacity-80 mt-0.5">(No formatting)</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="emailStatus === 'success'" class="mt-2 text-sm text-green-600">
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</NuxtLink>
|
||||
<ClientOnly>
|
||||
<Menu
|
||||
:is-authenticated="true"
|
||||
:is-admin="true"
|
||||
:is-authenticated="isAuthenticated"
|
||||
:is-admin="isAdmin"
|
||||
:show-home="true"
|
||||
:current-path="route.fullPath"
|
||||
/>
|
||||
@@ -378,18 +378,42 @@
|
||||
{{ retentionPolicySuccess }}
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<button
|
||||
@click="handleManualCleanup"
|
||||
:disabled="cleaningUp"
|
||||
class="px-6 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{ cleaningUp ? 'Running Cleanup...' : 'Run Cleanup Now' }}
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Manually trigger the cleanup process to delete sermons according to the retention policy.
|
||||
This will also run automatically on a daily schedule.
|
||||
</p>
|
||||
<div class="border-t pt-4 mt-4 space-y-4">
|
||||
<div>
|
||||
<button
|
||||
@click="handleAutoArchive"
|
||||
:disabled="autoArchiving"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{ autoArchiving ? 'Auto-Archiving...' : 'Run Auto-Archive Now' }}
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Automatically archive sermons that are 1 day past their most recent date.
|
||||
This moves them to the "Previous Sermons" list on the home page.
|
||||
This also runs automatically on a daily schedule.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
@click="handleManualCleanup"
|
||||
:disabled="cleaningUp"
|
||||
class="px-6 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{ cleaningUp ? 'Running Cleanup...' : 'Run Cleanup Now' }}
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Manually trigger the cleanup process to delete sermons according to the retention policy.
|
||||
This will also run automatically on a daily schedule.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="autoArchiveError" class="text-red-600 text-sm">
|
||||
{{ autoArchiveError }}
|
||||
</div>
|
||||
<div v-if="autoArchiveSuccess" class="text-green-600 text-sm">
|
||||
{{ autoArchiveSuccess }}
|
||||
</div>
|
||||
|
||||
<div v-if="cleanupError" class="text-red-600 text-sm">
|
||||
@@ -411,6 +435,11 @@ definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// Check authentication status
|
||||
const { data: authData } = await useFetch('/api/auth/verify')
|
||||
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
||||
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// Fetch all sermons for management (including archived)
|
||||
@@ -445,6 +474,9 @@ const retentionPolicy = ref('forever')
|
||||
const savingRetentionPolicy = ref(false)
|
||||
const retentionPolicyError = ref('')
|
||||
const retentionPolicySuccess = ref('')
|
||||
const autoArchiving = ref(false)
|
||||
const autoArchiveError = ref('')
|
||||
const autoArchiveSuccess = ref('')
|
||||
const cleaningUp = ref(false)
|
||||
const cleanupError = ref('')
|
||||
const cleanupSuccess = ref('')
|
||||
@@ -751,6 +783,27 @@ async function handleRetentionPolicyChange() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAutoArchive() {
|
||||
autoArchiveError.value = ''
|
||||
autoArchiveSuccess.value = ''
|
||||
autoArchiving.value = true
|
||||
|
||||
try {
|
||||
const result = await $fetch('/api/sermons/auto-archive', {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
autoArchiveSuccess.value = result.message
|
||||
|
||||
// Refresh sermon list after auto-archive
|
||||
await refreshSermons()
|
||||
} catch (e: any) {
|
||||
autoArchiveError.value = e.data?.message || 'Failed to run auto-archive'
|
||||
} finally {
|
||||
autoArchiving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleManualCleanup() {
|
||||
if (!confirm('Are you sure you want to run the cleanup process?\n\nThis will permanently delete all sermons older than the retention policy. This action cannot be undone.')) {
|
||||
return
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 flex flex-col">
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<NuxtLink to="/">
|
||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||
</NuxtLink>
|
||||
<ClientOnly fallback-tag="div">
|
||||
<Menu
|
||||
:is-authenticated="isAuthenticated"
|
||||
:is-admin="isAdmin"
|
||||
:show-home="true"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 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">Reset Password</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">{{ stepMessage }}</p>
|
||||
</div>
|
||||
@@ -171,6 +188,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Check authentication status
|
||||
const { data: authData } = await useFetch('/api/auth/verify')
|
||||
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
||||
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
||||
|
||||
const email = ref('')
|
||||
const code = ref('')
|
||||
const newPassword = ref('')
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
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"
|
||||
/>
|
||||
|
||||
|
||||
<!-- Password Match Indicator -->
|
||||
<div v-if="confirmPassword.length > 0" class="mt-2 text-xs">
|
||||
<div :class="passwordsMatch ? 'text-green-600' : 'text-red-600'">
|
||||
@@ -146,14 +146,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms Acceptance (only show when registering) -->
|
||||
<div v-if="isRegistering" class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
id="termsAccepted"
|
||||
v-model="termsAccepted"
|
||||
type="checkbox"
|
||||
required
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="termsAccepted" class="font-medium text-gray-700">
|
||||
I agree to the
|
||||
<button
|
||||
type="button"
|
||||
@click="showTermsModal = true"
|
||||
class="text-blue-600 hover:text-blue-700 underline"
|
||||
>
|
||||
Terms of Use & Privacy Policy
|
||||
</button>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
⚠️ This system was created with AI assistance and may contain errors. Please read the terms carefully.
|
||||
</p>
|
||||
</div>
|
||||
</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"
|
||||
:disabled="loading || (isRegistering && !termsAccepted)"
|
||||
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 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ loading ? (isRegistering ? 'Creating account...' : 'Signing in...') : (isRegistering ? 'Create Account' : 'Sign In') }}
|
||||
</button>
|
||||
@@ -180,8 +208,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Footer />
|
||||
<TermsModal v-model="showTermsModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -202,6 +231,8 @@ const firstName = ref('')
|
||||
const lastName = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const termsAccepted = ref(false)
|
||||
const showTermsModal = ref(false)
|
||||
|
||||
// Initialize from URL query parameter
|
||||
const isRegistering = ref(route.query.mode === 'register')
|
||||
@@ -233,6 +264,7 @@ function toggleMode() {
|
||||
email.value = ''
|
||||
firstName.value = ''
|
||||
lastName.value = ''
|
||||
termsAccepted.value = false
|
||||
|
||||
// Update URL to reflect mode
|
||||
const query = { ...route.query }
|
||||
@@ -284,6 +316,12 @@ async function handleRegister() {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate terms acceptance
|
||||
if (!termsAccepted.value) {
|
||||
error.value = 'You must agree to the Terms of Use & Privacy Policy'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
</NuxtLink>
|
||||
<ClientOnly>
|
||||
<Menu
|
||||
:is-authenticated="true"
|
||||
:is-admin="false"
|
||||
:is-authenticated="isAuthenticated"
|
||||
:is-admin="isAdmin"
|
||||
:show-home="true"
|
||||
:current-path="route.fullPath"
|
||||
/>
|
||||
@@ -248,6 +248,11 @@ definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// Check authentication status
|
||||
const { data: authData } = await useFetch('/api/auth/verify')
|
||||
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
||||
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const profile = ref({
|
||||
|
||||
235
pages/terms.vue
Normal file
235
pages/terms.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 flex flex-col">
|
||||
<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="hover:opacity-80">
|
||||
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
|
||||
</NuxtLink>
|
||||
<ClientOnly fallback-tag="div">
|
||||
<Menu
|
||||
:is-authenticated="isAuthenticated"
|
||||
:is-admin="isAdmin"
|
||||
:show-home="true"
|
||||
:current-path="route.fullPath"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 w-full">
|
||||
<div class="bg-white shadow-lg rounded-lg p-8">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-8">Terms of Use & Privacy Policy</h1>
|
||||
|
||||
<!-- AI Disclaimer Section -->
|
||||
<section class="mb-8 p-6 bg-yellow-50 border-l-4 border-yellow-400 rounded">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<span class="mr-2">⚠️</span> Important: AI-Generated Content Disclaimer
|
||||
</h2>
|
||||
<div class="space-y-3 text-gray-700">
|
||||
<p class="font-semibold">
|
||||
This website and application were created with artificial intelligence (AI) assistance.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Please be aware:</strong> AI technology, while powerful, is not perfect and can make mistakes.
|
||||
This includes but is not limited to:
|
||||
</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-2">
|
||||
<li>Errors in code or functionality</li>
|
||||
<li>Unexpected behavior or bugs</li>
|
||||
<li>Security vulnerabilities</li>
|
||||
<li>Data processing or storage issues</li>
|
||||
<li>Incorrect information display</li>
|
||||
</ul>
|
||||
<p class="font-semibold text-red-700 mt-4">
|
||||
Users should not rely on this system as the sole repository for important information.
|
||||
Always maintain backup copies of your sermon notes and important data.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Terms of Use -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Terms of Use</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<p>
|
||||
By accessing and using this website (the "Service"), you accept and agree to be bound by the
|
||||
terms and provisions of this agreement.
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">Purpose</h3>
|
||||
<p>
|
||||
This Service is provided by New Life Christian Church (the "Church") to assist members and
|
||||
attendees in viewing sermon information and taking personal notes during services. The Service
|
||||
is provided as-is for informational and organizational purposes only.
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">Use at Your Own Risk</h3>
|
||||
<p>
|
||||
You acknowledge and agree that your use of this Service is at your sole risk. The Service
|
||||
is provided on an "AS IS" and "AS AVAILABLE" basis without warranties of any kind, either
|
||||
express or implied.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Limitation of Liability -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Limitation of Liability</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<p class="font-semibold">
|
||||
TO THE FULLEST EXTENT PERMITTED BY LAW, NEW LIFE CHRISTIAN CHURCH, ITS STAFF, VOLUNTEERS,
|
||||
CONTRACTORS, AND AFFILIATES SHALL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING FROM
|
||||
THE USE OF THIS SERVICE.
|
||||
</p>
|
||||
<p>This includes but is not limited to:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-2">
|
||||
<li>Loss of data, including sermon notes or personal information</li>
|
||||
<li>Service interruptions or downtime</li>
|
||||
<li>Errors, bugs, or technical malfunctions</li>
|
||||
<li>Security breaches or unauthorized access</li>
|
||||
<li>Inaccurate or incomplete information</li>
|
||||
<li>Any indirect, incidental, special, consequential, or punitive damages</li>
|
||||
</ul>
|
||||
<p class="mt-4">
|
||||
The Church makes no guarantee regarding the availability, reliability, or accuracy of the
|
||||
Service and reserves the right to modify, suspend, or discontinue the Service at any time
|
||||
without notice.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Data & Privacy -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Privacy & Data</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">Information We Collect</h3>
|
||||
<p>When you register for an account, we collect:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-2">
|
||||
<li>Username and password (password is encrypted)</li>
|
||||
<li>Email address</li>
|
||||
<li>First and last name</li>
|
||||
<li>Your sermon notes and related content</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">How We Use Your Information</h3>
|
||||
<p>Your information is used to:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-2">
|
||||
<li>Provide access to the Service</li>
|
||||
<li>Store and manage your sermon notes</li>
|
||||
<li>Send you sermon notes via email when requested</li>
|
||||
<li>Authenticate your account</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">Data Retention</h3>
|
||||
<p>
|
||||
Your notes are stored in our database and are associated with your account.
|
||||
<strong class="text-red-700">Important:</strong> If a sermon is deleted by an administrator,
|
||||
all associated user notes for that sermon are permanently deleted. We recommend regularly
|
||||
downloading or emailing your notes to maintain personal backups.
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">Data Security</h3>
|
||||
<p>
|
||||
While we implement security measures to protect your data, no system is completely secure.
|
||||
Given that this system was created with AI assistance, there may be unknown vulnerabilities.
|
||||
The Church cannot guarantee the security of your information.
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold text-gray-900 mt-6 mb-3">Third-Party Services</h3>
|
||||
<p>
|
||||
When you email your notes, we use third-party email service providers to deliver your messages.
|
||||
Your email address and content are transmitted through these services.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- User Responsibilities -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">User Responsibilities</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<p>As a user of this Service, you agree to:</p>
|
||||
<ul class="list-disc list-inside ml-4 space-y-2">
|
||||
<li>Maintain the confidentiality of your account credentials</li>
|
||||
<li>Use the Service only for its intended purpose</li>
|
||||
<li>Maintain your own backups of important notes and information</li>
|
||||
<li>Not attempt to compromise the security of the Service</li>
|
||||
<li>Not use the Service for any unlawful purpose</li>
|
||||
<li>Accept sole responsibility for any content you create or upload</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Account Termination -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Account Termination</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<p>
|
||||
The Church reserves the right to terminate or suspend your account at any time, with or
|
||||
without notice, for any reason, including violation of these terms or at the Church's sole
|
||||
discretion.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Changes to Terms -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Changes to These Terms</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<p>
|
||||
The Church reserves the right to modify these terms at any time. Continued use of the
|
||||
Service after changes constitutes acceptance of the modified terms.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact -->
|
||||
<section class="mb-8">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Contact Information</h2>
|
||||
<div class="space-y-4 text-gray-700">
|
||||
<p>
|
||||
If you have questions about these terms or the Service, please contact New Life Christian
|
||||
Church through our regular church communication channels.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Acceptance -->
|
||||
<section class="mb-8 p-6 bg-gray-50 border rounded">
|
||||
<p class="text-gray-700">
|
||||
<strong>By using this Service, you acknowledge that you have read, understood, and agree to
|
||||
be bound by these Terms of Use and Privacy Policy, including the AI-generated content disclaimer.</strong>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 mt-4">
|
||||
Last Updated: November 7, 2025
|
||||
</p>
|
||||
</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 Home
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
// Check authentication status
|
||||
const { data: authData } = await useFetch('/api/auth/verify')
|
||||
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
||||
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
||||
</script>
|
||||
@@ -9,8 +9,8 @@
|
||||
</NuxtLink>
|
||||
<ClientOnly>
|
||||
<Menu
|
||||
:is-authenticated="true"
|
||||
:is-admin="true"
|
||||
:is-authenticated="isAuthenticated"
|
||||
:is-admin="isAdmin"
|
||||
:show-home="true"
|
||||
:current-path="route.fullPath"
|
||||
/>
|
||||
@@ -227,6 +227,8 @@ interface User {
|
||||
|
||||
const { data: authData } = await useFetch('/api/auth/verify')
|
||||
const currentUsername = computed(() => authData.value?.username)
|
||||
const isAuthenticated = computed(() => authData.value?.authenticated || false)
|
||||
const isAdmin = computed(() => authData.value?.isAdmin || false)
|
||||
|
||||
const users = ref<User[]>([])
|
||||
const loading = ref(true)
|
||||
|
||||
@@ -42,7 +42,26 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// Get user's notes
|
||||
const noteRecord = getSermonNote(user.id!, sermonId)
|
||||
const userNotes = noteRecord?.notes || 'No notes taken'
|
||||
// Convert HTML to plain text for download
|
||||
const htmlToText = (html: string) => {
|
||||
if (!html) return 'No notes taken'
|
||||
return html
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n')
|
||||
.replace(/<p[^>]*>/gi, '')
|
||||
.replace(/<\/h[1-6]>/gi, '\n')
|
||||
.replace(/<h[1-6][^>]*>/gi, '')
|
||||
.replace(/<li[^>]*>/gi, '• ')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.trim()
|
||||
}
|
||||
const userNotes = htmlToText(noteRecord?.notes || '')
|
||||
|
||||
// Format bible references
|
||||
let bibleReferencesText = ''
|
||||
|
||||
@@ -41,10 +41,28 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Get user's notes
|
||||
// Get user's notes (already stored as HTML from rich text editor)
|
||||
const noteRecord = getSermonNote(user.id!, sermonId)
|
||||
// Convert line breaks to HTML breaks for email display
|
||||
const userNotes = noteRecord?.notes ? noteRecord.notes.replace(/\n/g, '<br>') : ''
|
||||
let userNotes = noteRecord?.notes || '<p>No notes taken</p>'
|
||||
|
||||
// Convert Tiptap HTML to email-friendly HTML with inline styles
|
||||
// Email clients don't support <style> blocks well, so we need inline styles
|
||||
userNotes = userNotes
|
||||
// Convert <mark> to <span> with inline background color (email clients often don't support <mark>)
|
||||
// Using bright highlighter yellow (#FFEB3B) that stands out against the light yellow notes background
|
||||
.replace(/<mark>/gi, '<span style="background-color: #FFEB3B; padding: 2px 4px; border-radius: 2px;">')
|
||||
.replace(/<\/mark>/gi, '</span>')
|
||||
// Add inline styles to other elements
|
||||
.replace(/<strong>/gi, '<strong style="font-weight: 700;">')
|
||||
.replace(/<em>/gi, '<em style="font-style: italic;">')
|
||||
.replace(/<u>/gi, '<u style="text-decoration: underline;">')
|
||||
.replace(/<s>/gi, '<s style="text-decoration: line-through;">')
|
||||
.replace(/<h2>/gi, '<h2 style="font-size: 1.5em; font-weight: 700; margin: 1em 0 0.5em; color: #333;">')
|
||||
.replace(/<h3>/gi, '<h3 style="font-size: 1.25em; font-weight: 600; margin: 0.75em 0 0.5em; color: #333;">')
|
||||
.replace(/<ul>/gi, '<ul style="padding-left: 1.5em; margin: 0.5em 0;">')
|
||||
.replace(/<ol>/gi, '<ol style="padding-left: 1.5em; margin: 0.5em 0;">')
|
||||
.replace(/<li>/gi, '<li style="margin: 0.25em 0;">')
|
||||
.replace(/<p>/gi, '<p style="margin: 0.5em 0;">')
|
||||
|
||||
// Format bible references for HTML email
|
||||
let bibleReferencesText = ''
|
||||
|
||||
41
server/api/sermons/auto-archive.post.ts
Normal file
41
server/api/sermons/auto-archive.post.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getSessionUsername } from '~/server/utils/auth'
|
||||
import { autoArchiveOldSermons, getUserByUsername } from '~/server/utils/database'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Check authentication
|
||||
const username = await getSessionUsername(event)
|
||||
|
||||
if (!username) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized'
|
||||
})
|
||||
}
|
||||
|
||||
// Check admin role
|
||||
const user = getUserByUsername(username)
|
||||
|
||||
if (!user || user.is_admin !== 1) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Forbidden - Admin access required'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = autoArchiveOldSermons()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.archivedCount > 0
|
||||
? `Successfully auto-archived ${result.archivedCount} sermon(s)`
|
||||
: 'No sermons needed to be archived',
|
||||
archivedCount: result.archivedCount
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Failed to auto-archive sermons'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getSetting, deleteOldSermons, deleteExpiredSessions, getDatabase } from '../utils/database'
|
||||
import { getSetting, deleteOldSermons, deleteExpiredSessions, autoArchiveOldSermons, getDatabase } from '../utils/database'
|
||||
|
||||
// Map retention policy to days
|
||||
const retentionDaysMap: Record<string, number> = {
|
||||
@@ -12,6 +12,20 @@ const retentionDaysMap: Record<string, number> = {
|
||||
'10_years': 3650
|
||||
}
|
||||
|
||||
async function runSermonAutoArchive() {
|
||||
try {
|
||||
const result = autoArchiveOldSermons()
|
||||
|
||||
if (result.archivedCount > 0) {
|
||||
console.log(`[Sermon Auto-Archive] Archived ${result.archivedCount} sermon(s) that passed their most recent date`)
|
||||
} else {
|
||||
console.log(`[Sermon Auto-Archive] No sermons needed archiving`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Sermon Auto-Archive] Error running auto-archive:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function runSermonCleanup() {
|
||||
try {
|
||||
// Get the retention policy setting
|
||||
@@ -75,6 +89,7 @@ async function runPasswordResetCleanup() {
|
||||
|
||||
async function runAllCleanupTasks() {
|
||||
console.log('[Cleanup] Starting scheduled cleanup tasks')
|
||||
await runSermonAutoArchive()
|
||||
await runSermonCleanup()
|
||||
await runSessionCleanup()
|
||||
await runRateLimitCleanup()
|
||||
|
||||
@@ -372,6 +372,62 @@ export function archiveSermon(id: number) {
|
||||
return db.prepare('UPDATE sermons SET archived = 1 WHERE id = ?').run(id)
|
||||
}
|
||||
|
||||
export function unarchiveSermon(id: number) {
|
||||
const db = getDatabase()
|
||||
return db.prepare('UPDATE sermons SET archived = 0 WHERE id = ?').run(id)
|
||||
}
|
||||
|
||||
// Get the most recent date from a sermon (checking both primary date and additional dates)
|
||||
function getMostRecentSermonDate(sermon: any): Date {
|
||||
const dates: string[] = [sermon.date]
|
||||
|
||||
// Add additional dates if they exist
|
||||
if (sermon.dates) {
|
||||
try {
|
||||
const additionalDates = JSON.parse(sermon.dates)
|
||||
dates.push(...additionalDates)
|
||||
} catch {
|
||||
// If parsing fails, just use primary date
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all dates to Date objects and find the most recent
|
||||
const dateTimes = dates.map(dateStr => {
|
||||
const date = new Date(dateStr + 'T00:00:00')
|
||||
return date.getTime()
|
||||
})
|
||||
|
||||
return new Date(Math.max(...dateTimes))
|
||||
}
|
||||
|
||||
export function autoArchiveOldSermons(): { archivedCount: number } {
|
||||
const db = getDatabase()
|
||||
|
||||
// Get all non-archived sermons
|
||||
const sermons = db.prepare('SELECT * FROM sermons WHERE archived = 0').all() as any[]
|
||||
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0) // Start of today
|
||||
|
||||
let archivedCount = 0
|
||||
|
||||
for (const sermon of sermons) {
|
||||
const mostRecentDate = getMostRecentSermonDate(sermon)
|
||||
|
||||
// Calculate days difference
|
||||
const oneDayAfterSermon = new Date(mostRecentDate)
|
||||
oneDayAfterSermon.setDate(oneDayAfterSermon.getDate() + 1)
|
||||
|
||||
// If it's been at least 1 day since the most recent sermon date, archive it
|
||||
if (now >= oneDayAfterSermon) {
|
||||
archiveSermon(sermon.id!)
|
||||
archivedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return { archivedCount }
|
||||
}
|
||||
|
||||
export function getSermonBySlug(slug: string) {
|
||||
const db = getDatabase()
|
||||
return db.prepare('SELECT * FROM sermons WHERE slug = ?').get(slug) as Sermon | undefined
|
||||
|
||||
@@ -152,38 +152,43 @@ My Notes:
|
||||
${userNotes || 'No notes taken'}
|
||||
`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h1 style="color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px;">Sermon Notes</h1>
|
||||
|
||||
<div style="margin: 20px 0;">
|
||||
<h2 style="color: #4CAF50; margin-bottom: 5px;">${sermonTitle}</h2>
|
||||
<p style="color: #666; margin-top: 0;">${sermonDate}</p>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; margin: 0; padding: 0;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h1 style="color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; margin-top: 0;">Sermon Notes</h1>
|
||||
|
||||
<div style="background-color: #E3F2FD; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Bible References</h3>
|
||||
<div style="color: #333;">${bibleReferences}</div>
|
||||
</div>
|
||||
<div style="margin: 20px 0;">
|
||||
<h2 style="color: #4CAF50; margin-bottom: 5px; font-size: 1.5em;">${sermonTitle}</h2>
|
||||
<p style="color: #666; margin-top: 0;">${sermonDate}</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #E8F5E9; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #388E3C; margin-top: 0;">Personal Appliance</h3>
|
||||
<div style="color: #333; white-space: pre-wrap;">${personalAppliance}</div>
|
||||
</div>
|
||||
<div style="background-color: #E3F2FD; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0; font-size: 1.25em;">Bible References</h3>
|
||||
<div style="color: #333;">${bibleReferences}</div>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #F3E5F5; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #7B1FA2; margin-top: 0;">Pastor's Challenge</h3>
|
||||
<div style="color: #333; white-space: pre-wrap;">${pastorsChallenge}</div>
|
||||
</div>
|
||||
<div style="background-color: #E8F5E9; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #388E3C; margin-top: 0; font-size: 1.25em;">Personal Appliance</h3>
|
||||
<div style="color: #333; white-space: pre-wrap;">${personalAppliance}</div>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #FFF9C4; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #F57F17; margin-top: 0;">My Notes</h3>
|
||||
<div style="color: #333; white-space: pre-wrap;">${userNotes || '<em>No notes taken</em>'}</div>
|
||||
</div>
|
||||
<div style="background-color: #F3E5F5; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #7B1FA2; margin-top: 0; font-size: 1.25em;">Pastor's Challenge</h3>
|
||||
<div style="color: #333; white-space: pre-wrap;">${pastorsChallenge}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px;">
|
||||
<p>This email was sent from New Life Christian Church.</p>
|
||||
<div style="background-color: #FFF9C4; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
||||
<h3 style="color: #F57F17; margin-top: 0; font-size: 1.25em;">My Notes</h3>
|
||||
<div style="color: #333;">${userNotes || '<em>No notes taken</em>'}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px;">
|
||||
<p>This email was sent from New Life Christian Church.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user