mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:31:09 -05:00
Refactor photo management and upload functionality to use HTMX for asynchronous requests
- Updated photo upload handling in `photo_manager.html` and `photo_upload.html` to utilize HTMX for file uploads, improving user experience and reducing reliance on Promises. - Refactored caption update and primary photo toggle methods to leverage HTMX for state updates without full page reloads. - Enhanced error handling and success notifications using HTMX events. - Replaced fetch API calls with HTMX forms in various templates, including `homepage.html`, `park_form.html`, and `roadtrip_planner.html`, to streamline AJAX interactions. - Improved search suggestion functionality in `search_script.html` by implementing HTMX for fetching suggestions, enhancing performance and user experience. - Updated designer, manufacturer, and ride model forms to handle responses with HTMX, ensuring better integration and user feedback.
This commit is contained in:
@@ -126,7 +126,7 @@ document.addEventListener('alpine:init', () => {
|
||||
error: null,
|
||||
showSuccess: false,
|
||||
|
||||
async handleFileSelect(event) {
|
||||
handleFileSelect(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
if (!files.length) return;
|
||||
|
||||
@@ -146,23 +146,83 @@ document.addEventListener('alpine:init', () => {
|
||||
formData.append('object_id', objectId);
|
||||
|
||||
try {
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: formData
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-post', uploadUrl);
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
tempForm.enctype = 'multipart/form-data';
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
// Add form data
|
||||
const imageInput = document.createElement('input');
|
||||
imageInput.type = 'file';
|
||||
imageInput.name = 'image';
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
imageInput.files = dt.files;
|
||||
tempForm.appendChild(imageInput);
|
||||
|
||||
const appLabelInput = document.createElement('input');
|
||||
appLabelInput.type = 'hidden';
|
||||
appLabelInput.name = 'app_label';
|
||||
appLabelInput.value = contentType.split('.')[0];
|
||||
tempForm.appendChild(appLabelInput);
|
||||
|
||||
const modelInput = document.createElement('input');
|
||||
modelInput.type = 'hidden';
|
||||
modelInput.name = 'model';
|
||||
modelInput.value = contentType.split('.')[1];
|
||||
tempForm.appendChild(modelInput);
|
||||
|
||||
const objectIdInput = document.createElement('input');
|
||||
objectIdInput.type = 'hidden';
|
||||
objectIdInput.name = 'object_id';
|
||||
objectIdInput.value = objectId;
|
||||
tempForm.appendChild(objectIdInput);
|
||||
|
||||
// Use HTMX event listeners instead of Promise
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
try {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const photo = JSON.parse(event.detail.xhr.responseText);
|
||||
this.photos.push(photo);
|
||||
completedFiles++;
|
||||
this.uploadProgress = (completedFiles / totalFiles) * 100;
|
||||
|
||||
if (completedFiles === totalFiles) {
|
||||
this.uploading = false;
|
||||
this.showSuccess = true;
|
||||
setTimeout(() => {
|
||||
this.showSuccess = false;
|
||||
}, 3000);
|
||||
}
|
||||
} else {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
this.error = data.error || 'Upload failed';
|
||||
this.uploading = false;
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Upload failed';
|
||||
this.uploading = false;
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Upload failed';
|
||||
this.uploading = false;
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to upload photo. Please try again.';
|
||||
console.error('Upload error:', err);
|
||||
@@ -181,72 +241,125 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
async updateCaption(photo) {
|
||||
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
|
||||
})
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-post', `${uploadUrl}${photo.id}/caption/`);
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({ caption: photo.caption }));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.xhr.status < 200 || event.detail.xhr.status >= 300) {
|
||||
this.error = 'Failed to update caption';
|
||||
console.error('Caption update error');
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update caption');
|
||||
}
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Failed to update caption';
|
||||
console.error('Caption update error:', event.detail.error);
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to update caption';
|
||||
console.error('Caption update error:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async togglePrimary(photo) {
|
||||
togglePrimary(photo) {
|
||||
try {
|
||||
const response = await fetch(`${uploadUrl}${photo.id}/primary/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-post', `${uploadUrl}${photo.id}/primary/`);
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
// Update local state
|
||||
this.photos = this.photos.map(p => ({
|
||||
...p,
|
||||
is_primary: p.id === photo.id
|
||||
}));
|
||||
} else {
|
||||
this.error = 'Failed to update primary photo';
|
||||
console.error('Primary photo update error');
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
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
|
||||
}));
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Failed to update primary photo';
|
||||
console.error('Primary photo update error:', event.detail.error);
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to update primary photo';
|
||||
console.error('Primary photo update error:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async deletePhoto(photo) {
|
||||
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,
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-delete', `${uploadUrl}${photo.id}/`);
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
// Update local state
|
||||
this.photos = this.photos.filter(p => p.id !== photo.id);
|
||||
} else {
|
||||
this.error = 'Failed to delete photo';
|
||||
console.error('Delete error');
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete photo');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
this.photos = this.photos.filter(p => p.id !== photo.id);
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Failed to delete photo';
|
||||
console.error('Delete error:', event.detail.error);
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to delete photo';
|
||||
console.error('Delete error:', err);
|
||||
|
||||
@@ -128,7 +128,7 @@ document.addEventListener('alpine:init', () => {
|
||||
return this.photos.length < maxFiles;
|
||||
},
|
||||
|
||||
async handleFileSelect(event) {
|
||||
handleFileSelect(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
if (!files.length) return;
|
||||
|
||||
@@ -152,23 +152,79 @@ document.addEventListener('alpine:init', () => {
|
||||
formData.append('object_id', objectId);
|
||||
|
||||
try {
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: formData
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-post', uploadUrl);
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
tempForm.enctype = 'multipart/form-data';
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
// Add form data
|
||||
const imageInput = document.createElement('input');
|
||||
imageInput.type = 'file';
|
||||
imageInput.name = 'image';
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
imageInput.files = dt.files;
|
||||
tempForm.appendChild(imageInput);
|
||||
|
||||
const appLabelInput = document.createElement('input');
|
||||
appLabelInput.type = 'hidden';
|
||||
appLabelInput.name = 'app_label';
|
||||
appLabelInput.value = contentType.split('.')[0];
|
||||
tempForm.appendChild(appLabelInput);
|
||||
|
||||
const modelInput = document.createElement('input');
|
||||
modelInput.type = 'hidden';
|
||||
modelInput.name = 'model';
|
||||
modelInput.value = contentType.split('.')[1];
|
||||
tempForm.appendChild(modelInput);
|
||||
|
||||
const objectIdInput = document.createElement('input');
|
||||
objectIdInput.type = 'hidden';
|
||||
objectIdInput.name = 'object_id';
|
||||
objectIdInput.value = objectId;
|
||||
tempForm.appendChild(objectIdInput);
|
||||
|
||||
// Use HTMX event listeners instead of Promise
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
try {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
const photo = JSON.parse(event.detail.xhr.responseText);
|
||||
this.photos.push(photo);
|
||||
completedFiles++;
|
||||
this.uploadProgress = (completedFiles / totalFiles) * 100;
|
||||
|
||||
if (completedFiles === totalFiles) {
|
||||
this.uploading = false;
|
||||
}
|
||||
} else {
|
||||
const data = JSON.parse(event.detail.xhr.responseText);
|
||||
this.error = data.error || 'Upload failed';
|
||||
this.uploading = false;
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Upload failed';
|
||||
this.uploading = false;
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Upload failed';
|
||||
this.uploading = false;
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to upload photo. Please try again.';
|
||||
console.error('Upload error:', err);
|
||||
@@ -179,25 +235,43 @@ document.addEventListener('alpine:init', () => {
|
||||
event.target.value = ''; // Reset file input
|
||||
},
|
||||
|
||||
async togglePrimary(photo) {
|
||||
togglePrimary(photo) {
|
||||
try {
|
||||
const response = await fetch(`${uploadUrl}${photo.id}/primary/`, { // Added trailing slash
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-post', `${uploadUrl}${photo.id}/primary/`);
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
// Update local state
|
||||
this.photos = this.photos.map(p => ({
|
||||
...p,
|
||||
is_primary: p.id === photo.id
|
||||
}));
|
||||
} else {
|
||||
this.error = 'Failed to update primary photo';
|
||||
console.error('Primary photo update error');
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
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
|
||||
}));
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Failed to update primary photo';
|
||||
console.error('Primary photo update error:', event.detail.error);
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to update primary photo';
|
||||
console.error('Primary photo update error:', err);
|
||||
@@ -209,57 +283,92 @@ document.addEventListener('alpine:init', () => {
|
||||
this.showCaptionModal = true;
|
||||
},
|
||||
|
||||
async saveCaption() {
|
||||
saveCaption() {
|
||||
try {
|
||||
const response = await fetch(`${uploadUrl}${this.editingPhoto.id}/caption/`, { // Added trailing slash
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
caption: this.editingPhoto.caption
|
||||
})
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-post', `${uploadUrl}${this.editingPhoto.id}/caption/`);
|
||||
tempForm.setAttribute('hx-vals', JSON.stringify({ caption: this.editingPhoto.caption }));
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
// Update local state
|
||||
this.photos = this.photos.map(p =>
|
||||
p.id === this.editingPhoto.id
|
||||
? { ...p, caption: this.editingPhoto.caption }
|
||||
: p
|
||||
);
|
||||
|
||||
this.showCaptionModal = false;
|
||||
this.editingPhoto = { caption: '' };
|
||||
} else {
|
||||
this.error = 'Failed to update caption';
|
||||
console.error('Caption update error');
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update caption');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
this.photos = this.photos.map(p =>
|
||||
p.id === this.editingPhoto.id
|
||||
? { ...p, caption: this.editingPhoto.caption }
|
||||
: p
|
||||
);
|
||||
|
||||
this.showCaptionModal = false;
|
||||
this.editingPhoto = { caption: '' };
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Failed to update caption';
|
||||
console.error('Caption update error:', event.detail.error);
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to update caption';
|
||||
console.error('Caption update error:', err);
|
||||
}
|
||||
},
|
||||
|
||||
async deletePhoto(photo) {
|
||||
deletePhoto(photo) {
|
||||
if (!confirm('Are you sure you want to delete this photo?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${uploadUrl}${photo.id}/`, { // Added trailing slash
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
// Create temporary form for HTMX request
|
||||
const tempForm = document.createElement('form');
|
||||
tempForm.setAttribute('hx-delete', `${uploadUrl}${photo.id}/`);
|
||||
tempForm.setAttribute('hx-trigger', 'submit');
|
||||
tempForm.setAttribute('hx-swap', 'none');
|
||||
|
||||
// Add CSRF token
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = csrfToken;
|
||||
tempForm.appendChild(csrfInput);
|
||||
|
||||
tempForm.addEventListener('htmx:afterRequest', (event) => {
|
||||
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
||||
// Update local state
|
||||
this.photos = this.photos.filter(p => p.id !== photo.id);
|
||||
} else {
|
||||
this.error = 'Failed to delete photo';
|
||||
console.error('Delete error');
|
||||
}
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete photo');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
this.photos = this.photos.filter(p => p.id !== photo.id);
|
||||
|
||||
tempForm.addEventListener('htmx:error', (event) => {
|
||||
this.error = 'Failed to delete photo';
|
||||
console.error('Delete error:', event.detail.error);
|
||||
document.body.removeChild(tempForm);
|
||||
});
|
||||
|
||||
document.body.appendChild(tempForm);
|
||||
htmx.trigger(tempForm, 'submit');
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to delete photo';
|
||||
console.error('Delete error:', err);
|
||||
|
||||
Reference in New Issue
Block a user