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:
parent
6e7e27ac4e
commit
e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions
201
app/components/ImageUpload.vue
Normal file
201
app/components/ImageUpload.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue