Files
nlcc-itinerary/pages/[slug].vue
Joshua Ryder 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

389 lines
14 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 mb-3">
<NuxtLink to="/" class="hover:opacity-80">
<img src="/logos/logo.png" alt="New Life Christian Church" class="h-16 w-auto" />
</NuxtLink>
<div class="flex items-center gap-2">
<QRCodeButton v-if="sermon" :sermon="sermon" />
<ClientOnly fallback-tag="div">
<Menu
:is-authenticated="isAuthenticated"
:is-admin="isAdmin"
:show-home="true"
:current-path="route.fullPath"
/>
</ClientOnly>
</div>
</div>
<!-- Font Size Selector -->
<div class="flex items-center justify-end gap-2">
<label for="font-size" class="text-xs font-medium text-gray-700 whitespace-nowrap">Font Size:</label>
<select
id="font-size"
v-model="fontSize"
class="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm"
>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</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 v-if="sermon" class="bg-white shadow-lg rounded-lg p-8">
<!-- Header -->
<div class="border-b pb-6 mb-8">
<h1 class="text-4xl font-bold text-gray-900 mb-2">{{ sermon.title }}</h1>
<p class="text-lg text-gray-600">{{ formatDateRange(sermon) }}</p>
<ClientOnly>
<p v-if="isAdmin && sermon.creator_name" class="text-sm text-gray-500 mt-2">
Created by: {{ sermon.creator_name }}
</p>
</ClientOnly>
</div>
<!-- Section 1: Bible References -->
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Bible References</h2>
<div class="space-y-6">
<div
v-for="(ref, index) in bibleReferences"
:key="index"
class="bg-blue-50 rounded-lg p-6"
>
<!-- Mobile: Stack reference above text -->
<div class="md:hidden mb-3 text-right text-sm text-gray-600">
<div class="font-semibold">{{ ref.reference }}</div>
<div>({{ ref.version }})</div>
</div>
<!-- Desktop: Side by side -->
<div class="flex items-start justify-between gap-4">
<p :class="['text-gray-800 leading-relaxed flex-1', fontSizeClasses]">
{{ ref.text }}
</p>
<div class="hidden md:block text-right text-sm text-gray-600 whitespace-nowrap">
<div class="font-semibold">{{ ref.reference }}</div>
<div>({{ ref.version }})</div>
</div>
</div>
</div>
</div>
</section>
<!-- Section 2: Personal Appliance -->
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Personal Appliance</h2>
<div class="bg-green-50 rounded-lg p-6">
<p :class="['text-gray-800 whitespace-pre-wrap', fontSizeClasses]">{{ sermon.personal_appliance }}</p>
</div>
</section>
<!-- Section 3: Pastor's Challenge -->
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Pastor's Challenge</h2>
<div class="bg-purple-50 rounded-lg p-6">
<p :class="['text-gray-800 whitespace-pre-wrap', fontSizeClasses]">{{ sermon.pastors_challenge }}</p>
</div>
</section>
<!-- Worship Songs Section -->
<section v-if="worshipSongs.length > 0" class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Worship Songs</h2>
<div class="bg-yellow-50 rounded-lg p-6">
<ul class="space-y-3">
<li
v-for="(song, index) in worshipSongs"
:key="index"
:class="['text-gray-800', fontSizeClasses]"
>
<span class="font-semibold">{{ song.name }}</span>
<span v-if="song.artist" class="text-gray-600"> - {{ song.artist }}</span>
</li>
</ul>
</div>
</section>
<!-- Notes Section -->
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">My Notes</h2>
<ClientOnly>
<div v-if="isAuthenticated">
<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>
<RichTextEditor
v-model="notes"
@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 }}
</p>
<p v-else class="text-sm text-gray-500">Notes are automatically saved</p>
</div>
<div class="mt-4 flex gap-3">
<button
@click="emailNotes"
:disabled="emailStatus === 'sending'"
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 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"
>
<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">
Email sent successfully!
</p>
<p v-if="emailStatus === 'error'" class="mt-2 text-sm text-red-600">
Failed to send email. Please try again.
</p>
</div>
<div v-else class="bg-gray-50 rounded-lg p-8 text-center border-2 border-dashed border-gray-300">
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">Want to take notes?</h3>
<p class="text-gray-600 mb-4">Log in or create an account to save your sermon notes!</p>
<div class="flex gap-3 justify-center">
<NuxtLink
:to="`/login?redirect=${encodeURIComponent(route.fullPath)}`"
class="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-medium"
>
Log In
</NuxtLink>
<NuxtLink
to="/register"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Register
</NuxtLink>
</div>
</div>
</ClientOnly>
</section>
<!-- Back Button -->
<div class="border-t pt-6">
<NuxtLink
to="/"
class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to All Sermons
</NuxtLink>
</div>
</div>
<!-- Error State -->
<div v-else class="bg-white shadow-lg rounded-lg p-8 text-center">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Sermon Not Found</h2>
<p class="text-gray-600 mb-6">The sermon you're looking for doesn't exist.</p>
<NuxtLink
to="/"
class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to All Sermons
</NuxtLink>
</div>
</main>
<Footer />
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string
const { data: sermon } = await useFetch(`/api/sermons/${slug}`)
const { data: authData } = await useFetch('/api/auth/verify')
const isAuthenticated = computed(() => authData.value?.authenticated || false)
const isAdmin = computed(() => authData.value?.isAdmin || false)
// Font size state
const fontSize = ref('medium')
// Notes state
const notes = ref('')
const saveStatus = ref('')
const emailStatus = ref('')
let saveTimeout: NodeJS.Timeout | null = null
// Load notes when sermon is loaded and user is authenticated
watch([sermon, isAuthenticated], async ([sermonData, authenticated]) => {
if (sermonData && authenticated) {
try {
const response = await $fetch(`/api/notes/${sermonData.id}`)
notes.value = response.notes || ''
} catch (error) {
console.error('Failed to load notes:', error)
}
}
}, { immediate: true })
function handleNotesChange() {
saveStatus.value = 'Saving...'
// Clear existing timeout
if (saveTimeout) {
clearTimeout(saveTimeout)
}
// Set new timeout to save after 1 second of no typing
saveTimeout = setTimeout(async () => {
try {
await $fetch(`/api/notes/${sermon.value!.id}`, {
method: 'POST',
body: { notes: notes.value }
})
saveStatus.value = 'Saved'
setTimeout(() => {
saveStatus.value = ''
}, 2000)
} catch (error) {
saveStatus.value = 'Failed to save'
console.error('Failed to save notes:', error)
}
}, 1000)
}
async function handleLogout() {
await $fetch('/api/auth/logout', { method: 'POST' })
window.location.reload()
}
const bibleReferences = computed(() => {
if (!sermon.value?.bible_references) return []
try {
// Try to parse as JSON first (new format)
return JSON.parse(sermon.value.bible_references)
} catch {
// Fallback to old format (plain text)
return sermon.value.bible_references.split('\n')
.filter((ref: string) => ref.trim())
.map((ref: string) => ({ version: '', reference: ref, text: ref }))
}
})
const worshipSongs = computed(() => {
if (!sermon.value?.worship_songs) return []
try {
return JSON.parse(sermon.value.worship_songs)
} catch {
return []
}
})
// Font size classes
const fontSizeClasses = computed(() => {
switch (fontSize.value) {
case 'small':
return 'text-sm'
case 'large':
return 'text-xl'
default:
return 'text-base'
}
})
function formatDate(dateString: string) {
// Add T00:00:00 to ensure the date is interpreted as local time, not UTC
const date = new Date(dateString + 'T00:00:00')
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
function formatDateRange(sermon: any) {
// Helper function to format a single date with day name
const formatWithDayName = (dateString: string) => {
const date = new Date(dateString + 'T00:00:00')
const dayName = date.toLocaleDateString('en-US', { weekday: 'long' })
const dateStr = date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
return `${dayName}, ${dateStr}`
}
// Start with primary date
const dates = [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
}
}
// Format all dates and join with " - "
return dates.map(formatWithDayName).join(' - ')
}
// Email notes function
const emailNotes = async () => {
if (!sermon.value) return
emailStatus.value = 'sending'
try {
await $fetch(`/api/notes/email/${sermon.value.id}`, {
method: 'POST'
})
emailStatus.value = 'success'
setTimeout(() => {
emailStatus.value = ''
}, 3000)
} catch (error) {
console.error('Failed to email notes:', error)
emailStatus.value = 'error'
setTimeout(() => {
emailStatus.value = ''
}, 3000)
}
}
// Download notes function
const downloadNotes = () => {
if (!sermon.value) return
// Create a link and trigger download
const link = document.createElement('a')
link.href = `/api/notes/download/${sermon.value.id}`
link.download = `sermon-notes-${sermon.value.slug}.txt`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>