Revert "update"

This reverts commit 75cc618c2b.
This commit is contained in:
pacnpal
2025-09-21 20:11:00 -04:00
parent 75cc618c2b
commit 540f40e689
610 changed files with 4812 additions and 1715 deletions

View File

@@ -1,257 +0,0 @@
{% load static %}
<div x-data="photoManager({
photos: [
{% for photo in photos %}
{
id: {{ photo.id }},
url: '{{ photo.image.url }}',
caption: '{{ photo.caption|default:""|escapejs }}',
is_primary: {{ photo.is_primary|yesno:"true,false" }}
}{% if not forloop.last %},{% endif %}
{% endfor %}
],
contentType: '{{ content_type }}',
objectId: {{ object_id }},
csrfToken: '{{ csrf_token }}',
uploadUrl: '{% url "photos:upload" %}'
})" class="w-full">
<div class="relative space-y-6">
<!-- Upload Section -->
<div class="flex items-center justify-between">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Photos</h3>
<label class="cursor-pointer btn-secondary">
<i class="mr-2 fas fa-camera"></i>
<span>Upload Photo</span>
<input type="file"
class="hidden"
accept="image/*"
@change="handleFileSelect"
multiple>
</label>
</div>
<!-- Success Message -->
<div x-show="showSuccess"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="p-4 text-sm text-green-800 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-900">
Photo uploaded successfully!
</div>
<!-- Upload Progress -->
<template x-if="uploading">
<div class="p-4 bg-white rounded-lg shadow-lg dark:bg-gray-800">
<div class="flex items-center justify-between mb-1">
<span class="text-sm text-gray-700 dark:text-gray-300">Uploading...</span>
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="uploadProgress + '%'"></span>
</div>
<div class="w-full h-2 bg-gray-200 rounded-full dark:bg-gray-700">
<div class="h-2 transition-all duration-300 bg-blue-600 rounded-full"
:style="'width: ' + uploadProgress + '%'"></div>
</div>
</div>
</template>
<!-- Error Message -->
<template x-if="error">
<div class="p-4 text-sm text-red-800 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-900"
x-text="error"></div>
</template>
<!-- Photo Grid -->
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<template x-for="photo in photos" :key="photo.id">
<div class="relative p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50">
<!-- Photo -->
<div class="relative aspect-w-16 aspect-h-9 group">
<img :src="photo.url"
:alt="photo.caption || ''"
class="object-cover rounded-lg">
</div>
<!-- Caption -->
<div class="mt-4 space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Caption</label>
<textarea x-model="photo.caption"
@change="updateCaption(photo)"
class="w-full text-sm border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white"
rows="2"></textarea>
</div>
<!-- Actions -->
<div class="flex items-center justify-between mt-4">
<button @click="togglePrimary(photo)"
:class="{
'text-yellow-600 dark:text-yellow-400': photo.is_primary,
'text-gray-400 dark:text-gray-500': !photo.is_primary
}"
class="flex items-center gap-2 px-3 py-1 text-sm font-medium rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600">
<i class="fas" :class="photo.is_primary ? 'fa-star' : 'fa-star-o'"></i>
<span x-text="photo.is_primary ? 'Featured' : 'Set as Featured'"></span>
</button>
<button @click="deletePhoto(photo)"
class="flex items-center gap-2 px-3 py-1 text-sm font-medium text-red-600 rounded-lg dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20">
<i class="fas fa-trash"></i>
<span>Delete</span>
</button>
</div>
</div>
</template>
</div>
<!-- No Photos Message -->
<template x-if="photos.length === 0">
<div class="flex flex-col items-center justify-center py-12 text-center">
<i class="mb-4 text-4xl text-gray-400 fas fa-camera dark:text-gray-600"></i>
<p class="mb-4 text-gray-500 dark:text-gray-400">No photos available yet.</p>
<p class="text-sm text-gray-400 dark:text-gray-500">Click the upload button to add photos!</p>
</div>
</template>
</div>
</div>
<!-- AlpineJS Component Script -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('photoManager', ({ photos, contentType, objectId, csrfToken, uploadUrl }) => ({
photos,
uploading: false,
uploadProgress: 0,
error: null,
showSuccess: false,
async handleFileSelect(event) {
const files = Array.from(event.target.files);
if (!files.length) return;
this.uploading = true;
this.uploadProgress = 0;
this.error = null;
this.showSuccess = false;
const totalFiles = files.length;
let completedFiles = 0;
for (const file of files) {
const formData = new FormData();
formData.append('image', file);
formData.append('app_label', contentType.split('.')[0]);
formData.append('model', contentType.split('.')[1]);
formData.append('object_id', objectId);
try {
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
},
body: formData
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Upload failed');
}
const photo = await response.json();
this.photos.push(photo);
completedFiles++;
this.uploadProgress = (completedFiles / totalFiles) * 100;
} catch (err) {
this.error = err.message || 'Failed to upload photo. Please try again.';
console.error('Upload error:', err);
break;
}
}
this.uploading = false;
event.target.value = ''; // Reset file input
if (!this.error) {
this.showSuccess = true;
setTimeout(() => {
this.showSuccess = false;
}, 3000);
}
},
async updateCaption(photo) {
try {
const response = await fetch(`${uploadUrl}${photo.id}/caption/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
caption: photo.caption
})
});
if (!response.ok) {
throw new Error('Failed to update caption');
}
} catch (err) {
this.error = err.message || 'Failed to update caption';
console.error('Caption update error:', err);
}
},
async togglePrimary(photo) {
try {
const response = await fetch(`${uploadUrl}${photo.id}/primary/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error('Failed to update primary photo');
}
// Update local state
this.photos = this.photos.map(p => ({
...p,
is_primary: p.id === photo.id
}));
} catch (err) {
this.error = err.message || 'Failed to update primary photo';
console.error('Primary photo update error:', err);
}
},
async deletePhoto(photo) {
if (!confirm('Are you sure you want to delete this photo?')) {
return;
}
try {
const response = await fetch(`${uploadUrl}${photo.id}/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
}
});
if (!response.ok) {
throw new Error('Failed to delete photo');
}
// Update local state
this.photos = this.photos.filter(p => p.id !== photo.id);
} catch (err) {
this.error = err.message || 'Failed to delete photo';
console.error('Delete error:', err);
}
}
}));
});
</script>