Enhance application structure: Add runtime configuration for environment variables, integrate new dependencies for Cloudinary and UI components, and refactor member management features including improved forms and member dashboard. Update styles and layout for better user experience.

This commit is contained in:
Jennie Robinson Faber 2025-08-27 16:49:51 +01:00
parent 6e7e27ac4e
commit e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions

View file

@ -0,0 +1,201 @@
<template>
<div class="space-y-4">
<!-- Current Image Preview -->
<div v-if="modelValue?.url" class="relative">
<img
:src="transformedImageUrl"
:alt="modelValue.alt || 'Event image'"
class="w-full h-48 object-cover rounded-lg border border-gray-300"
@error="console.log('Image failed to load:', transformedImageUrl)"
@load="console.log('Image loaded successfully:', transformedImageUrl)"
/>
<button
@click="removeImage"
type="button"
class="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
>
<Icon name="heroicons:x-mark" class="w-4 h-4" />
</button>
</div>
<!-- Upload Area -->
<div
v-if="!modelValue?.url"
class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleDrop"
:class="{ 'border-blue-400 bg-blue-50': isDragging }"
>
<input
ref="fileInput"
type="file"
accept="image/*"
@change="handleFileSelect"
class="hidden"
/>
<div class="space-y-3">
<Icon name="heroicons:photo" class="w-12 h-12 text-gray-400 mx-auto" />
<div>
<p class="text-gray-600">
<button
type="button"
@click="$refs.fileInput.click()"
class="text-blue-600 hover:text-blue-500 font-medium"
>
Click to upload
</button>
or drag and drop
</p>
<p class="text-sm text-gray-500">PNG, JPG, GIF up to 10MB</p>
</div>
</div>
</div>
<!-- Alt Text Input -->
<div v-if="modelValue?.url">
<label class="block text-sm font-medium text-gray-700 mb-1">
Alt Text (for accessibility)
</label>
<input
:value="modelValue.alt || ''"
@input="updateAltText($event.target.value)"
placeholder="Describe this image..."
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<!-- Upload Progress -->
<div v-if="isUploading" class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Uploading...</span>
<span class="text-gray-600">{{ uploadProgress }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="`width: ${uploadProgress}%`"
/>
</div>
</div>
<!-- Error Message -->
<div v-if="errorMessage" class="text-sm text-red-600">
{{ errorMessage }}
</div>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: Object,
default: () => null
}
})
const emit = defineEmits(['update:modelValue'])
const isDragging = ref(false)
const isUploading = ref(false)
const uploadProgress = ref(0)
const errorMessage = ref('')
const fileInput = ref()
// Transform image URL for preview (smaller size)
const transformedImageUrl = computed(() => {
console.log('modelValue in computed:', props.modelValue)
// If we have the direct URL, use it
if (props.modelValue?.url) {
console.log('Using direct URL:', props.modelValue.url)
return props.modelValue.url
}
// Otherwise try to construct from publicId
if (props.modelValue?.publicId) {
const config = useRuntimeConfig()
const constructedUrl = `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/w_400,h_200,c_fill,f_auto,q_auto/${props.modelValue.publicId}`
console.log('Constructed URL:', constructedUrl)
return constructedUrl
}
console.log('No URL or publicId found')
return ''
})
const handleFileSelect = (event) => {
const file = event.target.files[0]
if (file) {
uploadFile(file)
}
}
const handleDrop = (event) => {
isDragging.value = false
const files = event.dataTransfer.files
if (files.length > 0) {
uploadFile(files[0])
}
}
const uploadFile = async (file) => {
// Validate file
if (!file.type.startsWith('image/')) {
errorMessage.value = 'Please select an image file'
return
}
if (file.size > 10 * 1024 * 1024) { // 10MB
errorMessage.value = 'File size must be less than 10MB'
return
}
errorMessage.value = ''
isUploading.value = true
uploadProgress.value = 0
try {
// Create form data for upload
const formData = new FormData()
formData.append('file', file)
// Upload to Cloudinary
const response = await $fetch(`/api/upload/image`, {
method: 'POST',
body: formData,
onUploadProgress: (progress) => {
uploadProgress.value = Math.round((progress.loaded / progress.total) * 100)
}
})
console.log('Upload response:', response)
// Update the model value
emit('update:modelValue', {
url: response.secure_url,
publicId: response.public_id,
alt: ''
})
} catch (error) {
console.error('Upload failed:', error)
errorMessage.value = 'Upload failed. Please try again.'
} finally {
isUploading.value = false
uploadProgress.value = 0
}
}
const removeImage = () => {
emit('update:modelValue', null)
}
const updateAltText = (altText) => {
emit('update:modelValue', {
...props.modelValue,
alt: altText
})
}
</script>