From 1f069503b3dc4a79131846f825190da49e21f316 Mon Sep 17 00:00:00 2001 From: kf212132 Date: Wed, 29 Apr 2026 18:56:26 -0500 Subject: [PATCH 1/3] fi --- app/components/event/Editor.vue | 105 ++-- app/components/event/EventImageUpload.vue | 106 ++++ app/components/event/Modal.vue | 158 ++---- app/pages/events/[id].vue | 454 ++++++------------ app/pages/events/manage.vue | 5 +- .../api/events/[id]/images/[name].delete.ts | 53 ++ server/api/events/[id]/images/[name].get.ts | 33 +- server/api/events/[id]/images/upload.post.ts | 39 +- server/api/events/index.get.ts | 3 +- server/utils/seed.ts | 2 +- 10 files changed, 441 insertions(+), 517 deletions(-) create mode 100644 app/components/event/EventImageUpload.vue create mode 100644 server/api/events/[id]/images/[name].delete.ts diff --git a/app/components/event/Editor.vue b/app/components/event/Editor.vue index 3128815..00a2417 100644 --- a/app/components/event/Editor.vue +++ b/app/components/event/Editor.vue @@ -1,30 +1,52 @@ - + + \ No newline at end of file diff --git a/app/components/event/Modal.vue b/app/components/event/Modal.vue index 8db52bc..e70cb0d 100644 --- a/app/components/event/Modal.vue +++ b/app/components/event/Modal.vue @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/app/pages/events/manage.vue b/app/pages/events/manage.vue index 13cfce6..28d20cc 100644 --- a/app/pages/events/manage.vue +++ b/app/pages/events/manage.vue @@ -55,7 +55,8 @@ async function navigateToEvent(eventId) { function getEventImage(event) { if (event.eventAssets && event.eventAssets.length > 0) { const imageUrl = event.eventAssets[0].imageUrl - return `/api/events/${event.id}/images/${imageUrl.split('/').pop()}` + const encoded = encodeURIComponent(imageUrl) + return `/api/events/${event.id}/images/${encoded}` } return null } @@ -144,7 +145,7 @@ function getEventDate(event) {

{{ event.title }}

-

{{ event.location }}

+

{{ event.location?.address }}

{{ getEventDate(event) }}

diff --git a/server/api/events/[id]/images/[name].delete.ts b/server/api/events/[id]/images/[name].delete.ts new file mode 100644 index 0000000..13177fb --- /dev/null +++ b/server/api/events/[id]/images/[name].delete.ts @@ -0,0 +1,53 @@ +import fs from 'node:fs' +import path from 'node:path' +import prisma from '~~/server/utils/prisma' + +export default defineEventHandler(async (event) => { + const fileName = getRouterParam(event, 'name') + const eventId = getRouterParam(event, 'id') + + if (!fileName) { + throw createError({ statusCode: 400, statusMessage: 'Missing fileName' }) + } + + if (!eventId) { + throw createError({ statusCode: 400, statusMessage: 'Missing eventId' }) + } + + // imageUrl stored in DB is just the fileName + const imageUrl = decodeURIComponent(fileName) + + // Check the asset exists in DB + const asset = await prisma.event_Asset.findUnique({ + where: { + eventId_imageUrl: { + eventId, + imageUrl, + } + } + }) + + if (!asset) { + throw createError({ statusCode: 404, statusMessage: 'Image not found' }) + } + + // Delete from DB + await prisma.event_Asset.delete({ + where: { + eventId_imageUrl: { + eventId, + imageUrl, + } + } + }) + + // Delete file from disk + const storageRoot = path.resolve(process.cwd(), process.env.IMAGE_STORAGE_PATH || 'public/images') + const filePath = path.join(storageRoot, eventId, decodeURIComponent(fileName)) + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath) + } + + return { message: 'Image deleted successfully' } +}) \ No newline at end of file diff --git a/server/api/events/[id]/images/[name].get.ts b/server/api/events/[id]/images/[name].get.ts index af6d949..1aa1e2c 100644 --- a/server/api/events/[id]/images/[name].get.ts +++ b/server/api/events/[id]/images/[name].get.ts @@ -6,37 +6,34 @@ export default defineEventHandler(async (event) => { const eventID = getRouterParam(event, 'id') if (!fileName) { - throw createError({ statusCode: 400, statusMessage: "Missing fileName" }) + throw createError({ statusCode: 400, statusMessage: 'Missing fileName' }) } - if(!eventID) { - throw createError({ statusCode: 400, statusMessage: "Missing eventID" }) + if (!eventID) { + throw createError({ statusCode: 400, statusMessage: 'Missing eventID' }) } - // Get file path relative to project root - const filePath = path.join( - process.env.IMAGE_STORAGE_PATH || "public/images", - eventID, - fileName - ) + const storageRoot = path.resolve(process.cwd(), process.env.IMAGE_STORAGE_PATH || 'public/images') + const filePath = path.join(storageRoot, eventID, decodeURIComponent(fileName)) + + console.log('๐Ÿ–ผ Serving image from:', filePath) + console.log('๐Ÿ“‚ File exists:', fs.existsSync(filePath)) if (!fs.existsSync(filePath)) { - throw createError({ statusCode: 404, statusMessage: "File not found" }) + throw createError({ statusCode: 404, statusMessage: 'File not found' }) } const fileStream = fs.createReadStream(filePath) - // Set content type based on file extension const ext = path.extname(filePath).toLowerCase() - const mime = - ext === ".png" ? "image/png" : - ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : - ext === ".gif" ? "image/gif" : - ext === ".webp" ? "image/webp" : - "application/octet-stream" + ext === '.png' ? 'image/png' : + ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : + ext === '.gif' ? 'image/gif' : + ext === '.webp' ? 'image/webp' : + 'application/octet-stream' - setHeader(event, "Content-Type", mime) + setHeader(event, 'Content-Type', mime) return sendStream(event, fileStream) }) \ No newline at end of file diff --git a/server/api/events/[id]/images/upload.post.ts b/server/api/events/[id]/images/upload.post.ts index 678ab6f..4cac215 100644 --- a/server/api/events/[id]/images/upload.post.ts +++ b/server/api/events/[id]/images/upload.post.ts @@ -11,13 +11,13 @@ export default defineEventHandler(async (event) => { } if (!form) { - throw createError({ statusCode: 400, statusMessage: "No form data" }) + throw createError({ statusCode: 400, statusMessage: 'No form data' }) } - const file = form.find(i => i.name === "file") + const file = form.find(i => i.name === 'file') if (!file || !file.data) { - throw createError({ statusCode: 400, statusMessage: "File missing" }) + throw createError({ statusCode: 400, statusMessage: 'File missing' }) } const foundEvent = await prisma.event.findUnique({ @@ -29,39 +29,36 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 404, message: 'Event not found' }) } - // Save file to public/images - const dirPath = path.join(process.env.IMAGE_STORAGE_PATH || "public/images", id) + const fileName = decodeURIComponent(file.filename || 'upload.png') + + // Use absolute path to ensure files are written to the right place + const storageRoot = path.resolve(process.cwd(), process.env.IMAGE_STORAGE_PATH || 'public/images') + const dirPath = path.join(storageRoot, id) + + console.log('๐Ÿ“ Writing image to:', dirPath) if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }) } - const filePath = path.join(dirPath, decodeURIComponent(file.filename || "failed.png")) + const filePath = path.join(dirPath, fileName) if (fs.existsSync(filePath)) { throw createError({ statusCode: 400, message: 'Image already exists.' }) } fs.writeFileSync(filePath, file.data) + console.log('โœ… File written:', filePath) - const addedImage = await prisma.event.update({ - where: { - id: id, - }, + await prisma.event.update({ + where: { id }, data: { eventAssets: { - create: [{ - imageUrl: path.join(id, "images", file.filename || "failed.png") - }] + create: [{ imageUrl: fileName }] } } }) - console.log(addedImage) - - setResponseStatus(event, 201); - - return { - message: "Added file to event." - } -}) + setResponseStatus(event, 201) + return { message: 'Added file to event.' } +}) \ No newline at end of file diff --git a/server/api/events/index.get.ts b/server/api/events/index.get.ts index 5c03980..c4ae4a4 100644 --- a/server/api/events/index.get.ts +++ b/server/api/events/index.get.ts @@ -6,7 +6,8 @@ export default defineEventHandler(async (_event) => { include: { eventAssets: true, volunteerHours: true, - participants: true + participants: true, + location: true, }, orderBy: { startTime: 'asc' diff --git a/server/utils/seed.ts b/server/utils/seed.ts index 6bfd6f4..d1a9304 100644 --- a/server/utils/seed.ts +++ b/server/utils/seed.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import type { Language, Gender, Availability, Ethinicity, ApprovalStatus } from "./generated/prisma/client.ts"; -import { PrismaClient } from './generated/prisma/client.ts'; +import { PrismaClient } from './generated/prisma/client'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; const adapter = new PrismaBetterSqlite3({ From 3bf0820b96dd31ab35bd0729841085680517f3f8 Mon Sep 17 00:00:00 2001 From: kf212132 Date: Thu, 30 Apr 2026 15:40:09 -0500 Subject: [PATCH 2/3] event signups --- app/components/event/EventRSVPModal.vue | 73 ++++++++++++++ app/components/event/EventRSVPStats.vue | 104 +++++++++++++++++++ app/components/event/Tile.vue | 36 ++++--- app/pages/events/[id].vue | 45 ++++++++- app/pages/events/index.vue | 128 +++++++++++++++++------- prisma/schema/event.prisma | 13 +++ server/api/events/[id]/rsvp.delete.ts | 32 ++++++ server/api/events/[id]/rsvp.get.ts | 32 ++++++ server/api/events/[id]/rsvp.post.ts | 46 +++++++++ 9 files changed, 462 insertions(+), 47 deletions(-) create mode 100644 app/components/event/EventRSVPModal.vue create mode 100644 app/components/event/EventRSVPStats.vue create mode 100644 server/api/events/[id]/rsvp.delete.ts create mode 100644 server/api/events/[id]/rsvp.get.ts create mode 100644 server/api/events/[id]/rsvp.post.ts diff --git a/app/components/event/EventRSVPModal.vue b/app/components/event/EventRSVPModal.vue new file mode 100644 index 0000000..5e85806 --- /dev/null +++ b/app/components/event/EventRSVPModal.vue @@ -0,0 +1,73 @@ + + + \ No newline at end of file diff --git a/app/components/event/EventRSVPStats.vue b/app/components/event/EventRSVPStats.vue new file mode 100644 index 0000000..e58946a --- /dev/null +++ b/app/components/event/EventRSVPStats.vue @@ -0,0 +1,104 @@ + + + \ No newline at end of file diff --git a/app/components/event/Tile.vue b/app/components/event/Tile.vue index 576600e..62ab93c 100644 --- a/app/components/event/Tile.vue +++ b/app/components/event/Tile.vue @@ -1,34 +1,46 @@ + \ No newline at end of file + diff --git a/app/pages/events/index.vue b/app/pages/events/index.vue index 7f1df5f..4813d00 100644 --- a/app/pages/events/index.vue +++ b/app/pages/events/index.vue @@ -1,50 +1,110 @@ - + \ No newline at end of file diff --git a/prisma/schema/event.prisma b/prisma/schema/event.prisma index 018220d..47a47f9 100644 --- a/prisma/schema/event.prisma +++ b/prisma/schema/event.prisma @@ -17,6 +17,7 @@ model Event { eventAssets Event_Asset[] participants RSVP[] volunteerHours Volunteer_Hour_Log[] + guestRSVPs GuestRSVP[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -45,3 +46,15 @@ model RSVP { @@id([userId, eventId]) @@map("rsvps") } + +model GuestRSVP { + id String @id @default(uuid()) + eventId String + event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) + name String + email String + isVolunteer Boolean @default(false) + createdAt DateTime @default(now()) + + @@map("guest_rsvps") +} diff --git a/server/api/events/[id]/rsvp.delete.ts b/server/api/events/[id]/rsvp.delete.ts new file mode 100644 index 0000000..3102bc9 --- /dev/null +++ b/server/api/events/[id]/rsvp.delete.ts @@ -0,0 +1,32 @@ +import prisma from '~~/server/utils/prisma' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + const body = await readBody(event) + + if (!id) { + throw createError({ statusCode: 400, message: 'Missing event ID' }) + } + + if (!body.email) { + throw createError({ statusCode: 400, message: 'Email is required' }) + } + + const rsvp = await prisma.guestRSVP.findFirst({ + where: { + eventId: id, + email: body.email.toLowerCase(), + isVolunteer: body.isVolunteer ?? false, + } + }) + + if (!rsvp) { + throw createError({ statusCode: 404, message: 'RSVP not found' }) + } + + await prisma.guestRSVP.delete({ + where: { id: rsvp.id } + }) + + return { message: 'RSVP removed successfully' } +}) \ No newline at end of file diff --git a/server/api/events/[id]/rsvp.get.ts b/server/api/events/[id]/rsvp.get.ts new file mode 100644 index 0000000..b9cf34a --- /dev/null +++ b/server/api/events/[id]/rsvp.get.ts @@ -0,0 +1,32 @@ +import prisma from '~~/server/utils/prisma' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ statusCode: 400, message: 'Missing event ID' }) + } + + const foundEvent = await prisma.event.findUnique({ + where: { id }, + }) + + if (!foundEvent) { + throw createError({ statusCode: 404, message: 'Event not found' }) + } + + const rsvps = await prisma.guestRSVP.findMany({ + where: { eventId: id }, + orderBy: { createdAt: 'asc' } + }) + + const volunteers = rsvps.filter(r => r.isVolunteer) + const attendees = rsvps.filter(r => !r.isVolunteer) + + return { + volunteerCount: volunteers.length, + attendeeCount: attendees.length, + volunteers, + attendees, + } +}) \ No newline at end of file diff --git a/server/api/events/[id]/rsvp.post.ts b/server/api/events/[id]/rsvp.post.ts new file mode 100644 index 0000000..c8d3f39 --- /dev/null +++ b/server/api/events/[id]/rsvp.post.ts @@ -0,0 +1,46 @@ +import prisma from '~~/server/utils/prisma' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + const body = await readBody(event) + + if (!id) { + throw createError({ statusCode: 400, message: 'Missing event ID' }) + } + + if (!body.name || !body.email) { + throw createError({ statusCode: 400, message: 'Name and email are required' }) + } + + const foundEvent = await prisma.event.findUnique({ + where: { id } + }) + + if (!foundEvent) { + throw createError({ statusCode: 404, message: 'Event not found' }) + } + + const existing = await prisma.guestRSVP.findFirst({ + where: { + eventId: id, + email: body.email.toLowerCase(), + isVolunteer: body.isVolunteer ?? false, + } + }) + + if (existing) { + throw createError({ statusCode: 409, message: 'You are already signed up for this event' }) + } + + const rsvp = await prisma.guestRSVP.create({ + data: { + eventId: id, + name: body.name.trim(), + email: body.email.toLowerCase().trim(), + isVolunteer: body.isVolunteer ?? false, + } + }) + + setResponseStatus(event, 201) + return rsvp +}) \ No newline at end of file From e3adb0a2e72f03a650ee5540ede2ab02e0483bd9 Mon Sep 17 00:00:00 2001 From: Vikas Thoutam Date: Wed, 13 May 2026 00:07:06 -0500 Subject: [PATCH 3/3] chore: update event image fetching to more consistently display images --- .../{EventImageUpload.vue => ImageUpload.vue} | 26 +- app/components/event/Modal.vue | 26 +- .../{EventRSVPModal.vue => RSVPModal.vue} | 41 +- .../{EventRSVPStats.vue => RSVPStats.vue} | 80 ++- app/components/event/Tile.vue | 5 + app/pages/events/[id].vue | 10 +- app/pages/events/index.vue | 3 +- app/pages/events/manage.vue | 30 +- app/pages/index.vue | 620 +++++++++--------- .../api/events/[id]/images/[name].delete.ts | 12 +- server/api/events/[id]/index.patch.ts | 7 +- server/api/events/[id]/rsvp.delete.ts | 6 +- server/api/events/[id]/rsvp.get.ts | 4 +- server/api/events/[id]/rsvp.post.ts | 8 +- server/api/volunteer-logs/index.get.ts | 37 +- 15 files changed, 493 insertions(+), 422 deletions(-) rename app/components/event/{EventImageUpload.vue => ImageUpload.vue} (89%) rename app/components/event/{EventRSVPModal.vue => RSVPModal.vue} (71%) rename app/components/event/{EventRSVPStats.vue => RSVPStats.vue} (51%) diff --git a/app/components/event/EventImageUpload.vue b/app/components/event/ImageUpload.vue similarity index 89% rename from app/components/event/EventImageUpload.vue rename to app/components/event/ImageUpload.vue index 9c416f6..05b1a73 100644 --- a/app/components/event/EventImageUpload.vue +++ b/app/components/event/ImageUpload.vue @@ -42,30 +42,33 @@ function onFilesChanged(files: File[] | null | undefined) { emit('filesChanged', safeFiles) } +function getAssetUrl(asset: ServerAsset) { + return `/api/events/${asset.imageUrl}` +} + async function deleteExistingAsset(asset: ServerAsset) { if (!props.eventId) return try { - await $fetch(`/api/events/${props.eventId}/images/${asset.imageUrl}`, { - method: 'DELETE' + await $fetch(getAssetUrl(asset), { + method: 'DELETE', }) remainingAssets.value = remainingAssets.value.filter(a => a.imageUrl !== asset.imageUrl) emit('assetDeleted', asset.imageUrl) - } catch (err) { + } + catch (err) { console.error('Failed to delete image:', err) } } - -function getAssetUrl(asset: ServerAsset) { - return `/api/events/${props.eventId}/images/${asset.imageUrl}` -} \ No newline at end of file + diff --git a/app/components/event/Modal.vue b/app/components/event/Modal.vue index ae0a840..5d66dab 100644 --- a/app/components/event/Modal.vue +++ b/app/components/event/Modal.vue @@ -17,29 +17,11 @@ const newEvent = ref({ const isSaving = ref(false) // Store actual File objects for upload -const filesToUpload = ref([]) +const filesToUpload = ref([] as File[]) -watch(filesToUpload, async (newFiles) => { - if (!newFiles || newFiles.length === 0) { - newEvent.value.eventAssets = [] - return - } - const previews = await Promise.all( - Array.from(newFiles).map( - file => - new Promise((resolve) => { - const reader = new FileReader() - reader.onload = e => - resolve({ - imageUrl: e.target.result, - fileName: file.name, - }) - reader.readAsDataURL(file) - }), - ), - ) - newEvent.value.eventAssets = previews -}) +function onFilesChanged(files: File[]) { + filesToUpload.value = files +} async function saveEvent() { // Validate required fields diff --git a/app/components/event/EventRSVPModal.vue b/app/components/event/RSVPModal.vue similarity index 71% rename from app/components/event/EventRSVPModal.vue rename to app/components/event/RSVPModal.vue index 5e85806..002fe50 100644 --- a/app/components/event/EventRSVPModal.vue +++ b/app/components/event/RSVPModal.vue @@ -28,12 +28,14 @@ async function submit() { name: name.value, email: email.value, isVolunteer: props.isVolunteer, - } + }, }) emit('success') - } catch (err: any) { + } + catch (err: any) { error.value = err?.data?.message || 'Something went wrong. Please try again.' - } finally { + } + finally { isSubmitting.value = false } } @@ -52,22 +54,43 @@ async function submit() {

- + - + -

{{ error }}

+

+ {{ error }} +

- + Cancel - + {{ isVolunteer ? 'Sign Up' : 'Register' }}
- \ No newline at end of file + diff --git a/app/components/event/EventRSVPStats.vue b/app/components/event/RSVPStats.vue similarity index 51% rename from app/components/event/EventRSVPStats.vue rename to app/components/event/RSVPStats.vue index e58946a..0bbbbf8 100644 --- a/app/components/event/EventRSVPStats.vue +++ b/app/components/event/RSVPStats.vue @@ -29,7 +29,9 @@ defineExpose({ refresh }) \ No newline at end of file + diff --git a/app/components/event/Tile.vue b/app/components/event/Tile.vue index eb29e56..eb0b8e7 100644 --- a/app/components/event/Tile.vue +++ b/app/components/event/Tile.vue @@ -5,6 +5,7 @@ const props = defineProps<{ subtitle?: string buttonType?: 'plus' | 'arrow' eventId?: string + imageUrl?: string }>() const iconName = computed(() => @@ -12,6 +13,10 @@ const iconName = computed(() => ? 'i-heroicons-arrow-right-20-solid' : 'i-heroicons-plus', ) + +function getAssetUrl(imageUrl: string) { + return `/api/events/${imageUrl}` +}