Notes!
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
37
server/api/notes/[sermonId].get.ts
Normal file
37
server/api/notes/[sermonId].get.ts
Normal 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 || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
50
server/api/notes/[sermonId].post.ts
Normal file
50
server/api/notes/[sermonId].post.ts
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user