Compare commits

..

12 Commits

Author SHA1 Message Date
7f11b36fa4 Updated image url 2025-11-09 18:39:25 -05:00
ede88a7d7d Updated image url 2025-11-09 17:33:29 -05:00
57715f0e92 refactor: Simplify footer to center terms link
Removed redundant AI warning from footer since AI assistance is
already mentioned in the creation line. Now just shows:
- Created for New Life Christian Church by Joshua Ryder with AI assistance
- Terms of Use & Privacy Policy (centered link)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:29:30 -05:00
6269655ae1 fix: Add patch-package to resolve Docker build error
Added patch-package as a devDependency to fix build failure where
rollup's postinstall script was failing due to missing patch-package.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:24:20 -05:00
1c2b80d837 fix: Correct terms last updated date to November 7, 2025
Fixed incorrect date (was January 7, 2025) to the correct date of
November 7, 2025 in both the terms modal and terms page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:20:34 -05:00
8c66550987 feat: Add comprehensive terms & conditions with required acceptance
- Create responsive TermsModal component for mobile and desktop
- Add comprehensive legal terms with prominent AI disclaimer
- Update footer to show terms modal and AI warning on all pages
- Require terms acceptance checkbox during account registration
- Add validation to ensure users accept terms before registration
- Include clickable link in checkbox to view full terms in modal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:14:36 -05:00
fc17db62d4 fix: Use brighter yellow for email highlighting
Change highlight color from pale yellow (#fef08a) to bright highlighter yellow (#FFEB3B) for better visibility against the light yellow notes section background in emails.

The Material Design yellow provides strong contrast while remaining pleasant to read, matching the look of a real highlighter marker.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 10:27:46 -05:00
752e74b2ed fix: Convert rich text to inline styles for reliable email rendering
Fix highlighting and all rich text formatting in emails by converting HTML tags to inline styles before sending. Email clients strip <style> blocks, so inline styles are the only reliable method.

Changes:
- Convert <mark> tags to <span> with inline background-color (#fef08a)
- Add inline styles to all rich text elements (strong, em, u, s, headings, lists)
- Process HTML conversion in email API before sending
- Simplified email template by removing unreliable <style> block
- All formatting now uses inline styles directly on elements

This ensures highlights and all other formatting (bold, italic, underline, strikethrough, headings, lists) render correctly across all email clients including Gmail, Outlook, Apple Mail, etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 10:21:53 -05:00
1515fba6c9 fix: Improve rich text formatting in emails and add button hints
Fix highlighting display in emailed notes and add clear formatting hints to email/download buttons.

Changes:
- Added proper HTML/CSS structure to email template for rich text support
- Added CSS styles for mark (highlight), strong, em, u, s, headings, and lists
- Highlight now renders with yellow background (#fef08a) in emails
- All rich text formatting now properly displays in email clients
- Added formatting hints to buttons: "Email Notes (Formatting included)" and "Download Notes (No formatting)"
- Button hints use smaller text with opacity for subtle visual hierarchy

Email template improvements:
- Proper DOCTYPE and HTML structure
- Style block in head for rich text elements
- Removed white-space: pre-wrap from notes div to allow HTML rendering
- Maintained all existing sermon content styling

This ensures users understand that:
- Email preserves all rich text formatting (bold, italic, highlights, lists, etc.)
- Download converts to plain text for universal compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 10:16:31 -05:00
0ff37e8999 feat: Implement automatic sermon archiving based on dates
Add intelligent auto-archiving system that automatically moves sermons to the "Previous Sermons" list when they are 1 day past their most recent date.

Features:
- Auto-archive logic that checks both primary and additional sermon dates
- Finds the most recent date across all dates for a sermon
- Archives sermon 1 day after the most recent date has passed
- Manual trigger via "Run Auto-Archive Now" button on admin page
- Automatic daily execution via scheduled cleanup task
- Clear admin UI with explanatory text and status messages
- Manual archive/unarchive functionality preserved

Implementation:
- Added getMostRecentSermonDate() helper to find latest date from primary and additional dates
- Added autoArchiveOldSermons() function to database utils
- Created /api/sermons/auto-archive endpoint for manual triggering
- Integrated into daily cleanup plugin schedule
- Updated admin UI with auto-archive button and status indicators
- Added unarchiveSermon() function for completeness

The system runs automatically every 24 hours and can be manually triggered by admins. Sermons are moved to the previous sermons dropdown on the home page exactly 1 day after their final presentation date, ensuring the main page always shows current and upcoming content while preserving access to past sermons.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 09:53:17 -05:00
3a50cbebdd feat: Add rich text formatting to sermon notes
Implement comprehensive rich text editing capabilities using Tiptap editor with full formatting toolbar and functionality.

Features:
- Bold, italic, underline, strikethrough text formatting
- Highlight text with yellow marker
- Bullet and numbered lists
- Heading styles (H2, H3)
- Text alignment (left, center, right)
- Undo/redo support
- Clear formatting option
- Intuitive toolbar with icons and tooltips
- Responsive font size support

Technical changes:
- Added Tiptap dependencies to package.json (@tiptap/vue-3, starter-kit, extensions)
- Created RichTextEditor component with full toolbar and formatting options
- Integrated editor into sermon notes section replacing textarea
- Updated download API to convert HTML to formatted plain text
- Updated email API to handle HTML content properly
- Notes now stored as HTML with rich formatting preserved

The editor provides a professional writing experience with all standard formatting tools while maintaining automatic save functionality and seamless integration with email/download features.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 09:47:23 -05:00
3f1c573a67 feat: Make menu context-aware and hide current page links
Implement intelligent menu filtering that hides self-referencing links and shows all applicable pages based on user role and authentication state.

Changes:
- Menu component now uses currentPath to filter out the current page link
- Added computed properties to detect which page user is on
- Fixed profile, admin, and users pages to dynamically detect admin status from API
- Added menu to forgot-password page for consistent navigation
- All pages now pass correct authentication state to Menu component

This ensures menus always show relevant navigation options while avoiding redundant links to the current page. Admin users now see all admin options (Manage Sermons, Manage Users) regardless of which page they're on, except the current one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 09:40:42 -05:00
19 changed files with 1214 additions and 68 deletions

View File

@@ -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>

View File

@@ -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

View 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
View 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>

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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">

View File

@@ -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

View File

@@ -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('')

View File

@@ -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 {

View File

@@ -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
View 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>

View File

@@ -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)

View File

@@ -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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.trim()
}
const userNotes = htmlToText(noteRecord?.notes || '')
// Format bible references
let bibleReferencesText = ''

View File

@@ -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 = ''

View 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'
})
}
})

View File

@@ -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()

View File

@@ -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

View File

@@ -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>
`,
}