From 2dbd4f6ba0e9459ffcdff569bd3d23d59fdba899 Mon Sep 17 00:00:00 2001 From: Joshua Ryder Date: Mon, 6 Oct 2025 17:20:26 -0400 Subject: [PATCH] Notes! --- pages/[slug].vue | 80 +++++++++++++++++++++++++++++ server/api/notes/[sermonId].get.ts | 37 +++++++++++++ server/api/notes/[sermonId].post.ts | 50 ++++++++++++++++++ server/utils/database.ts | 44 ++++++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 server/api/notes/[sermonId].get.ts create mode 100644 server/api/notes/[sermonId].post.ts diff --git a/pages/[slug].vue b/pages/[slug].vue index eeb1d9d..c3f1ee5 100644 --- a/pages/[slug].vue +++ b/pages/[slug].vue @@ -95,6 +95,41 @@ + +
+

My Notes

+ +
+ +
+

+ {{ saveStatus }} +

+

Notes are automatically saved

+
+
+
+ + + +

Want to take notes?

+

Log in or create an account to save your sermon notes!

+ + Log In / Sign Up + +
+
+
+
authData.value?.authenticated || false) // Font size state 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(() => { if (!sermon.value?.bible_references) return [] try { diff --git a/server/api/notes/[sermonId].get.ts b/server/api/notes/[sermonId].get.ts new file mode 100644 index 0000000..cecce4e --- /dev/null +++ b/server/api/notes/[sermonId].get.ts @@ -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 || '' + } +}) diff --git a/server/api/notes/[sermonId].post.ts b/server/api/notes/[sermonId].post.ts new file mode 100644 index 0000000..6f1008e --- /dev/null +++ b/server/api/notes/[sermonId].post.ts @@ -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' + }) + } +}) diff --git a/server/utils/database.ts b/server/utils/database.ts index d08edac..3d53aaf 100644 --- a/server/utils/database.ts +++ b/server/utils/database.ts @@ -24,6 +24,15 @@ export interface User { 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() { if (!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 const config = useRuntimeConfig() const adminUsername = config.adminUsername @@ -148,3 +171,24 @@ export function resetUserPassword(id: number, newPassword: string) { const hashedPassword = bcrypt.hashSync(newPassword, saltRounds) 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) +}