Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 92 additions & 32 deletions app/components/event/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,53 @@ const props = defineProps({

const emit = defineEmits(['save', 'delete'])

const editedEvent = ref<any>({})
const filesToUpload = ref<File[]>([])
// Build asset list for the uploader from the real event assets
const eventAssets = ref<{ imageUrl: string, isPreview?: boolean, fileName?: string }[]>([])
const editedEvent = ref({
name: '',
date: '',
location: '',
image: '',
})

// Watch for changes to the event prop and update the form
watch(() => props.event, (newEvent) => {
if (newEvent) {
editedEvent.value = { ...newEvent }
// Map existing server assets to uploader format
// imageUrl in DB is just the fileName (after the upload fix)
eventAssets.value = (newEvent.eventAssets || []).map((a: any) => ({
imageUrl: `/api/events/${newEvent.id}/images/${a.imageUrl}`,
fileName: a.imageUrl,
isPreview: false,
}))
}
}, { immediate: true })

function saveEvent() {
function onFilesChanged(files: File[]) {
filesToUpload.value = files
}

async function saveEvent() {
// Upload any pending files first
if (filesToUpload.value.length > 0) {
for (const file of filesToUpload.value) {
const formData = new FormData()
formData.append('file', file)
try {
await $fetch(`/api/events/${editedEvent.value.id}/images/upload`, {
method: 'POST',
body: formData,
})
}
catch (err) {
console.error(`Failed to upload ${file.name}:`, err)
}
}
filesToUpload.value = []
}

emit('save', { ...editedEvent.value })
}

Expand All @@ -39,41 +71,69 @@ function deleteEvent() {
Edit Event
</h3>

<UFormField label="Event Name">
<UInput v-model="editedEvent.name" />
</UFormField>
<UFormGroup label="Event Title">
<UInput
v-model="editedEvent.title"
placeholder="Event title"
/>
</UFormGroup>

<UFormField label="Date">
<UFormGroup label="Short Description">
<UInput
v-model="editedEvent.date"
type="date"
v-model="editedEvent.shortDesc"
placeholder="Brief description"
/>
</UFormField>

<UFormField label="Location">
<UInput v-model="editedEvent.location" />
</UFormField>

<UFormField label="Image URL">
<UInput v-model="editedEvent.image" />
</UFormField>

<!-- Preview image if URL provided -->
<div
v-if="editedEvent.image"
class="mt-2"
>
<p class="text-sm text-gray-600 mb-2">
Image Preview:
</p>
<img
:src="editedEvent.image"
alt="Event preview"
class="w-full h-32 object-cover rounded-lg"
@error="$event.target.style.display = 'none'"
>
</UFormGroup>

<UFormGroup label="Full Description">
<UTextarea
v-model="editedEvent.description"
:rows="4"
placeholder="Full description"
/>
</UFormGroup>

<UFormGroup label="Location">
<UInput
v-model="editedEvent.location.address"
placeholder="Location address"
/>
</UFormGroup>

<div class="grid grid-cols-2 gap-4">
<UFormGroup label="Start Date & Time">
<UInput
v-model="editedEvent.startTime"
type="datetime-local"
/>
</UFormGroup>
<UFormGroup label="End Date & Time">
<UInput
v-model="editedEvent.endTime"
type="datetime-local"
/>
</UFormGroup>
</div>

<div class="space-y-2">
<label class="flex items-center gap-2 cursor-pointer">
<UCheckbox v-model="editedEvent.allowVolunteers" />
<span class="text-sm font-medium text-gray-700">Allow Volunteers</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<UCheckbox v-model="editedEvent.allowAttendees" />
<span class="text-sm font-medium text-gray-700">Allow Attendees</span>
</label>
</div>

<UFormGroup label="Event Images">
<EventImageUpload
:existing-assets="editedEvent.eventAssets"
:event-id="editedEvent.id"
@files-changed="onFilesChanged"
/>
</UFormGroup>

<div class="flex justify-between pt-2">
<UButton
color="red"
Expand Down
112 changes: 112 additions & 0 deletions app/components/event/ImageUpload.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script setup lang="ts">
/**
* EventImageUpload
*
* Wraps Nuxt UI's UFileUpload for use in event forms.
* - Handles multiple image selection with drag & drop
* - Shows existing server images alongside new uploads
* - Supports deleting existing images from the server
*/

interface ServerAsset {
imageUrl: string // just the fileName as stored in DB
}

const props = defineProps<{
// Existing saved assets from the server (pass event.eventAssets)
existingAssets?: ServerAsset[]
// The event ID, needed to build image URLs and delete from server
eventId?: string
}>()

const emit = defineEmits<{
// Emits the raw File[] whenever selection changes, for parent to upload on save
(e: 'filesChanged', files: File[]): void
// Emits when an existing server image is deleted
(e: 'assetDeleted', imageUrl: string): void
}>()

// Files selected by the user (not yet uploaded)
const selectedFiles = ref<File[]>([])

// Track which existing assets are still present
const remainingAssets = ref<ServerAsset[]>([...(props.existingAssets || [])])

watch(() => props.existingAssets, (assets) => {
remainingAssets.value = [...(assets || [])]
}, { deep: true })

function onFilesChanged(files: File[] | null | undefined) {
const safeFiles = files ?? []
selectedFiles.value = safeFiles
emit('filesChanged', safeFiles)
}

function getAssetUrl(asset: ServerAsset) {
return `/api/events/${asset.imageUrl}`
}

async function deleteExistingAsset(asset: ServerAsset) {
if (!props.eventId) return

try {
await $fetch(getAssetUrl(asset), {
method: 'DELETE',
})
remainingAssets.value = remainingAssets.value.filter(a => a.imageUrl !== asset.imageUrl)
emit('assetDeleted', asset.imageUrl)
}
catch (err) {
console.error('Failed to delete image:', err)
}
}
</script>

<template>
<div class="space-y-4">
<!-- Existing server images -->
<div v-if="remainingAssets.length > 0">
<p class="text-sm text-gray-500 mb-2">
Current Images
</p>
<div class="grid grid-cols-3 gap-2">
<div
v-for="asset in remainingAssets"
:key="asset.imageUrl"
class="relative group aspect-square rounded-lg overflow-hidden bg-gray-100"
>
<img
:src="getAssetUrl(asset)"
:alt="asset.imageUrl"
class="w-full h-full object-cover"
>
<button
class="absolute top-1 right-1 bg-red-500 text-white p-1 rounded-full shadow"
type="button"
@click="deleteExistingAsset(asset)"
>
<UIcon
name="i-lucide-trash-2"
class="w-3 h-3"
/>
</button>
</div>
</div>
</div>

<!-- UFileUpload for new images -->
<UFileUpload
v-model="selectedFiles"
multiple
accept="image/*"
variant="area"
layout="list"
icon="i-lucide-image"
label="Drop images here or click to browse"
description="PNG, JPG, WEBP, GIF supported"
color="brand4"
class="w-full min-h-36"
@update:model-value="onFilesChanged"
/>
</div>
</template>
Loading