This commit is contained in:
2025-10-06 17:20:26 -04:00
parent 291b6743c5
commit 2dbd4f6ba0
4 changed files with 211 additions and 0 deletions

View File

@@ -95,6 +95,41 @@
</div> </div>
</section> </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">
<textarea
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>
<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>
<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>
<NuxtLink
to="/login"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log In / Sign Up
</NuxtLink>
</div>
</ClientOnly>
</section>
<!-- Back Button --> <!-- Back Button -->
<div class="border-t pt-6"> <div class="border-t pt-6">
<NuxtLink <NuxtLink
@@ -134,10 +169,55 @@ const route = useRoute()
const slug = route.params.slug as string const slug = route.params.slug as string
const { data: sermon } = await useFetch(`/api/sermons/${slug}`) const { data: sermon } = await useFetch(`/api/sermons/${slug}`)
const { data: authData } = await useFetch('/api/auth/verify')
const isAuthenticated = computed(() => authData.value?.authenticated || false)
// Font size state // Font size state
const fontSize = ref('medium') const fontSize = ref('medium')
// Notes state
const notes = ref('')
const saveStatus = 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)
}
const bibleReferences = computed(() => { const bibleReferences = computed(() => {
if (!sermon.value?.bible_references) return [] if (!sermon.value?.bible_references) return []
try { try {

View File

@@ -0,0 +1,37 @@
import { getSermonNote, getUserByUsername } from '~/server/utils/database'
import { getAuthCookie } from '~/server/utils/auth'
export default defineEventHandler(async (event) => {
const username = getAuthCookie(event)
if (!username) {
throw createError({
statusCode: 401,
message: 'Unauthorized'
})
}
const user = getUserByUsername(username)
if (!user) {
throw createError({
statusCode: 401,
message: 'User not found'
})
}
const sermonId = parseInt(event.context.params?.sermonId || '')
if (isNaN(sermonId)) {
throw createError({
statusCode: 400,
message: 'Invalid sermon ID'
})
}
const note = getSermonNote(user.id!, sermonId)
return {
notes: note?.notes || ''
}
})

View File

@@ -0,0 +1,50 @@
import { saveSermonNote, getUserByUsername } from '~/server/utils/database'
import { getAuthCookie } from '~/server/utils/auth'
export default defineEventHandler(async (event) => {
const username = getAuthCookie(event)
if (!username) {
throw createError({
statusCode: 401,
message: 'Unauthorized'
})
}
const user = getUserByUsername(username)
if (!user) {
throw createError({
statusCode: 401,
message: 'User not found'
})
}
const sermonId = parseInt(event.context.params?.sermonId || '')
const body = await readBody(event)
const { notes } = body
if (isNaN(sermonId)) {
throw createError({
statusCode: 400,
message: 'Invalid sermon ID'
})
}
if (typeof notes !== 'string') {
throw createError({
statusCode: 400,
message: 'Notes must be a string'
})
}
try {
saveSermonNote(user.id!, sermonId, notes)
return { success: true }
} catch (error) {
throw createError({
statusCode: 500,
message: 'Failed to save notes'
})
}
})

View File

@@ -24,6 +24,15 @@ export interface User {
is_admin: number is_admin: number
} }
export interface SermonNote {
id?: number
user_id: number
sermon_id: number
notes: string
created_at?: string
updated_at?: string
}
export function getDatabase() { export function getDatabase() {
if (!db) { if (!db) {
const dbPath = join(process.cwd(), 'data', 'sermons.db') const dbPath = join(process.cwd(), 'data', 'sermons.db')
@@ -55,6 +64,20 @@ export function getDatabase() {
) )
`) `)
db.exec(`
CREATE TABLE IF NOT EXISTS sermon_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
sermon_id INTEGER NOT NULL,
notes TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (sermon_id) REFERENCES sermons(id) ON DELETE CASCADE,
UNIQUE(user_id, sermon_id)
)
`)
// Insert default admin user from environment variables with hashed password // Insert default admin user from environment variables with hashed password
const config = useRuntimeConfig() const config = useRuntimeConfig()
const adminUsername = config.adminUsername const adminUsername = config.adminUsername
@@ -148,3 +171,24 @@ export function resetUserPassword(id: number, newPassword: string) {
const hashedPassword = bcrypt.hashSync(newPassword, saltRounds) const hashedPassword = bcrypt.hashSync(newPassword, saltRounds)
return db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hashedPassword, id) return db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hashedPassword, id)
} }
export function getSermonNote(userId: number, sermonId: number) {
const db = getDatabase()
return db.prepare('SELECT * FROM sermon_notes WHERE user_id = ? AND sermon_id = ?').get(userId, sermonId) as SermonNote | undefined
}
export function saveSermonNote(userId: number, sermonId: number, notes: string) {
const db = getDatabase()
const existing = getSermonNote(userId, sermonId)
if (existing) {
return db.prepare('UPDATE sermon_notes SET notes = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND sermon_id = ?').run(notes, userId, sermonId)
} else {
return db.prepare('INSERT INTO sermon_notes (user_id, sermon_id, notes) VALUES (?, ?, ?)').run(userId, sermonId, notes)
}
}
export function deleteSermonNote(userId: number, sermonId: number) {
const db = getDatabase()
return db.prepare('DELETE FROM sermon_notes WHERE user_id = ? AND sermon_id = ?').run(userId, sermonId)
}