fix(album): removing last album image was not working

This commit is contained in:
2025-06-02 12:35:44 -04:00
parent a08b384495
commit 6ae6db0c1d
7 changed files with 369 additions and 262 deletions

14
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"useTabs": false,
"tabWidth": 4,
"printWidth": 120,
"semi": true,
"singleQuote": true,
"singleAttributePerLine": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"htmlWhitespaceSensitivity": "ignore",
"vueIndentScriptAndStyle": true,
"endOfLine": "lf"
}

View File

@@ -32,10 +32,13 @@
"devDependencies": { "devDependencies": {
"@types/webpack-env": "^1.18.8", "@types/webpack-env": "^1.18.8",
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^10.2.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.18.0",
"eslint-plugin-vue": "^9.22.0", "eslint-plugin-vue": "^9.22.0",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"vite": "^6.3.1" "vite": "^6.3.1"
} }
@@ -975,6 +978,18 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@pkgr/core": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
"integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@rollup/pluginutils": { "node_modules/@rollup/pluginutils": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
@@ -1554,6 +1569,20 @@
"rfdc": "^1.4.1" "rfdc": "^1.4.1"
} }
}, },
"node_modules/@vue/eslint-config-prettier": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz",
"integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==",
"dev": true,
"dependencies": {
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2"
},
"peerDependencies": {
"eslint": ">= 8.21.0",
"prettier": ">= 3.0.0"
}
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.14", "version": "3.5.14",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz",
@@ -2460,6 +2489,67 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "10.1.5",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"funding": {
"url": "https://opencollective.com/eslint-config-prettier"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
"integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
"dev": true,
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.11.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint-plugin-prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-tailwindcss": {
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.18.0.tgz",
"integrity": "sha512-PQDU4ZMzFH0eb2DrfHPpbgo87Zgg2EXSMOj1NSfzdZm+aJzpuwGerfowMIaVehSREEa0idbf/eoNYAOHSJoDAQ==",
"dev": true,
"dependencies": {
"fast-glob": "^3.2.5",
"postcss": "^8.4.4"
},
"engines": {
"node": ">=18.12.0"
},
"peerDependencies": {
"tailwindcss": "^3.4.0"
}
},
"node_modules/eslint-plugin-vue": { "node_modules/eslint-plugin-vue": {
"version": "9.33.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
@@ -2617,6 +2707,12 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -4090,6 +4186,33 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/property-expr": { "node_modules/property-expr": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
@@ -4626,6 +4749,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/synckit": {
"version": "0.11.6",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz",
"integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==",
"dev": true,
"dependencies": {
"@pkgr/core": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",

View File

@@ -33,10 +33,13 @@
"devDependencies": { "devDependencies": {
"@types/webpack-env": "^1.18.8", "@types/webpack-env": "^1.18.8",
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^10.2.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.18.0",
"eslint-plugin-vue": "^9.22.0", "eslint-plugin-vue": "^9.22.0",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"vite": "^6.3.1" "vite": "^6.3.1"
} }

View File

@@ -1,40 +1,27 @@
<template> <template>
<div class="p-4 relative" <div class="relative p-4"
@mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id" @mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id"
@mouseleave="showEditButtons = false"> @mouseleave="showEditButtons = false">
<!-- Edit buttons with absolute positioning --> <!-- Edit buttons with absolute positioning -->
<div v-if="showEditButtons || isEditMode" <div v-if="showEditButtons || isEditMode" class="absolute right-4 top-4 flex gap-2">
class="absolute top-4 right-4 flex gap-2">
<!-- Edit button with pencil icon --> <!-- Edit button with pencil icon -->
<button <button v-if="!isEditMode" :title="t('edit')"
v-if="!isEditMode" class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
:title="t('edit')" @click="toggleEditMode()">
class="w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
@click="toggleEditMode()"
>
<v-icon large>mdi-pencil</v-icon> <v-icon large>mdi-pencil</v-icon>
</button> </button>
<!-- Save button --> <!-- Save button -->
<button <button v-if="isEditMode" :disabled="!canSave" :title="t('save')"
v-if="isEditMode" class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg" @click="saveChanges()">
:disabled="!canSave"
:title="t('save')"
class="w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
@click="saveChanges()"
>
<v-icon large>mdi-check</v-icon> <v-icon large>mdi-check</v-icon>
</button> </button>
<!-- Cancel button --> <!-- Cancel button -->
<button <button v-if="isEditMode" :title="t('cancel')"
v-if="isEditMode" class="flex size-12 items-center justify-center rounded-full bg-red-500 shadow-lg" @click="cancelEdit">
:title="t('cancel')"
class="w-12 h-12 bg-red-500 rounded-full flex items-center justify-center shadow-lg"
@click="cancelEdit"
>
<v-icon large>mdi-close</v-icon> <v-icon large>mdi-close</v-icon>
</button> </button>
</div> </div>
@@ -42,7 +29,7 @@
<!-- MainPage --> <!-- MainPage -->
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="flex justify-start text-2xl font-bold text-center mb-4"> <h1 class="mb-4 flex justify-start text-center text-2xl font-bold">
{{ t('creator.sections.about.title') }} {{ t('creator.sections.about.title') }}
</h1> </h1>
@@ -50,72 +37,47 @@
<!-- Description Section --> <!-- Description Section -->
<div> <div>
<div v-if="!isEditMode"> <div v-if="!isEditMode">
<p v-if="description" class="text-lg text-justify mb-6 whitespace-pre-line"> <p v-if="description" class="mb-6 whitespace-pre-line text-justify text-lg">
{{ description }} {{ description }}
</p> </p>
</div> </div>
<v-textarea v-if="isEditMode" <v-textarea v-if="isEditMode" v-model="editableDescription" :counter="2000" :error-messages="descriptionError"
v-model="editableDescription" :label="t('creator.sections.about.description')" :rules="[
:counter="2000"
:error-messages="descriptionError"
:label="t('creator.sections.about.description')"
:rules="[
v => !!v || t('creator.validation.descriptionRequired'), v => !!v || t('creator.validation.descriptionRequired'),
v => v.length <= 2000 || t('creator.validation.descriptionTooLong') v => v.length <= 2000 || t('creator.validation.descriptionTooLong')
]" ]" auto-grow class="w-full p-2 py-6" rows="5" variant="outlined"></v-textarea>
auto-grow
class="w-full p-2 py-6"
rows="5"
variant="outlined"></v-textarea>
</div> </div>
<!-- Video Section --> <!-- Video Section -->
<div v-if="videoUrl || isEditMode" <div v-if="videoUrl || isEditMode" :class="['content-section', {
:class="['content-section', {
'rounded-t-xl': hasImages && !isEditMode, 'rounded-t-xl': hasImages && !isEditMode,
'rounded-xl': !hasImages && !isEditMode 'rounded-xl': !hasImages && !isEditMode
}]"> }]">
<div v-if="!isEditMode && videoUrl" class="video-container"> <div v-if="!isEditMode && videoUrl" class="video-container">
<iframe <iframe :src="youtubeEmbedUrl"
:src="youtubeEmbedUrl"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen allowfullscreen class="video-frame" title="YouTube video player">
class="video-frame"
title="YouTube video player">
</iframe> </iframe>
</div> </div>
<div v-if="isEditMode"> <div v-if="isEditMode">
<v-text-field <v-text-field v-model="editableVideoUrl" :error-messages="videoUrlError"
v-model="editableVideoUrl" :label="t('creator.fields.videoUrl')" class="w-full p-2" type="text" variant="outlined" />
:error-messages="videoUrlError"
:label="t('creator.fields.videoUrl')"
class="w-full p-2"
type="text"
variant="outlined"
/>
</div> </div>
</div> </div>
<!-- Photos Section using Album component --> <!-- Photos Section using Album component -->
<div> <div>
<!-- Use AlbumView for display mode --> <!-- Use AlbumView for display mode -->
<AlbumView v-if="!isEditMode && hasImages" <AlbumView v-if="!isEditMode && hasImages" :class="['content-section', {
:class="['content-section', {
'rounded-b-xl': videoUrl && !isEditMode, 'rounded-b-xl': videoUrl && !isEditMode,
'rounded-xl': !videoUrl && !isEditMode 'rounded-xl': !videoUrl && !isEditMode
}]" }]" :images="thumbnailUrls" @photo-click="handlePhotoClick" />
:images="thumbnailUrls"
@photo-click="handlePhotoClick"/>
<AlbumViewer v-model="showAlbumViewer" <AlbumViewer v-model="showAlbumViewer" :images="originalUrls" :start-index="selectedPhotoIndex" />
:images="originalUrls"
:start-index="selectedPhotoIndex"/>
<!-- Use AlbumEditor for edit mode --> <!-- Use AlbumEditor for edit mode -->
<AlbumEditor v-if="isEditMode" <AlbumEditor v-if="isEditMode" :images="photos" @update:images="updateImages" />
:images="thumbnailUrls"
@update:images="updateImages"/>
</div> </div>
<!-- Contact Information Section --> <!-- Contact Information Section -->
@@ -144,7 +106,6 @@ import {useI18n} from 'vue-i18n';
import { buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId } from '@/utils/youtube'; import { buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId } from '@/utils/youtube';
import AlbumEditor from "@/views/creators/AlbumEditor.vue"; import AlbumEditor from "@/views/creators/AlbumEditor.vue";
import AlbumView from "@/views/creators/AlbumView.vue"; import AlbumView from "@/views/creators/AlbumView.vue";
// Add these imports at the top with your other imports
import AlbumViewer from './AlbumViewer.vue'; import AlbumViewer from './AlbumViewer.vue';
const { t } = useI18n(); const { t } = useI18n();
@@ -162,7 +123,7 @@ const description = ref("");
const videoUrl = ref(""); const videoUrl = ref("");
const phoneNumber = ref(""); const phoneNumber = ref("");
const email = ref(""); const email = ref("");
const thumbnailUrls = ref([]); const photos = ref([]); //before was thumbnailUrls
const albumId = ref(null); const albumId = ref(null);
const originalPhotos = ref([]); const originalPhotos = ref([]);
// Add these refs with your other refs // Add these refs with your other refs
@@ -195,10 +156,19 @@ const canSave = computed(() => {
return true; return true;
}); });
const thumbnailUrls = computed(() => {
return photos.value.map(photo => photo.thumbnailUrl)
})
// Add this computed property to get the original image URLs
const originalUrls = computed(() => {
return photos.value.map(photo => photo.originalUrl);
});
// Computed property to check if there are images // Computed property to check if there are images
const hasImages = computed(() => { const hasImages = computed(() => {
// Only consider it has images if there are actual image URLs (not empty strings) // Only consider it has images if there are actual image URLs (not empty strings)
return thumbnailUrls.value.length > 0 && thumbnailUrls.value.some(img => img && img.trim() !== ""); return photos.value.length > 0;
}); });
// Computed property for YouTube embed URL // Computed property for YouTube embed URL
@@ -253,17 +223,17 @@ async function fetchAlbumData() {
// Store original photos for comparison // Store original photos for comparison
originalPhotos.value = response.data.photos; originalPhotos.value = response.data.photos;
// Extract photo URLs from the album photos // Extract photo URLs from the album photos
thumbnailUrls.value = response.data.photos.map(photo => photo.thumbnailUrl); photos.value = response.data.photos;
} else { } else {
// Initialize with empty array instead of empty slots // Initialize with empty array instead of empty slots
thumbnailUrls.value = []; photos.value = [];
originalPhotos.value = []; originalPhotos.value = [];
} }
} catch (error) { } catch (error) {
// Album might not exist yet, which is fine // Album might not exist yet, which is fine
console.log("Album might not exist yet:", error); console.log("Album might not exist yet:", error);
// Initialize with empty array instead of empty slots // Initialize with empty array instead of empty slots
thumbnailUrls.value = []; photos.value = [];
originalPhotos.value = []; originalPhotos.value = [];
} }
} }
@@ -283,7 +253,7 @@ onMounted(async () => {
// Update images from Album component // Update images from Album component
function updateImages(newImages) { function updateImages(newImages) {
thumbnailUrls.value = newImages; photos.value = newImages;
} }
async function saveChanges() { async function saveChanges() {
@@ -325,8 +295,15 @@ async function saveChanges() {
description.value = editableDescription.value; description.value = editableDescription.value;
videoUrl.value = extractVideoId(editableVideoUrl.value) || ""; videoUrl.value = extractVideoId(editableVideoUrl.value) || "";
// Check for deleted photos
const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
// If the photo URL is not in the current images array, it was deleted
return !photos.value.includes(originalPhoto.thumbnailUrl);
});
// Save album photos if they've changed // Save album photos if they've changed
if (thumbnailUrls.value.length > 0) { if (photos.value.length > 0 || deletedPhotos.length > 0) {
// Create or update the album // Create or update the album
const albumId = brandingStore.value.id; const albumId = brandingStore.value.id;
@@ -342,12 +319,6 @@ async function saveChanges() {
console.log("Album might already exist:", error); console.log("Album might already exist:", error);
} }
// Check for deleted photos
const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
// If the photo URL is not in the current images array, it was deleted
return !thumbnailUrls.value.includes(originalPhoto.thumbnailUrl);
});
// Delete removed photos // Delete removed photos
for (const photo of deletedPhotos) { for (const photo of deletedPhotos) {
try { try {
@@ -358,17 +329,30 @@ async function saveChanges() {
} }
// Now add or update photos // Now add or update photos
for (let i = 0; i < thumbnailUrls.value.length; i++) { for (let i = 0; i < photos.value.length; i++) {
const imageUrl = thumbnailUrls.value[i]; const imageData = photos.value[i];
if (imageUrl && imageUrl.startsWith('data:')) { console.log('Image Data to be uploaded:', imageData);
if (imageData && imageData.image && imageData.image.originalUrl.startsWith('data:')) {
// This is a new image that needs to be uploaded // This is a new image that needs to be uploaded
const photoId = crypto.randomUUID(); const photoId = crypto.randomUUID();
const formData = new FormData(); const formData = new FormData();
// Extract MIME type from data URL
const mimeMatch = imageData.image.originalUrl.match(/^data:(.*?);base64,/);
if (!mimeMatch) {
console.warn(`Invalid data URL at index ${i}`);
continue;
}
const mimeType = mimeMatch[1];
// Determine file extension from MIME type
const extension = mimeType.split('/')[1]; // e.g., "jpeg", "png", "webp", etc.
// Convert data URL to file // Convert data URL to file
const response = await fetch(imageUrl); const response = await fetch(imageData.image.originalUrl);
const blob = await response.blob(); const blob = await response.blob();
const file = new File([blob], `photo-${i}.jpg`, {type: 'image/jpeg'}); const file = new File([blob], `photo-${i}.${extension}`, { type: mimeType });
formData.append('file', file); formData.append('file', file);
@@ -404,12 +388,6 @@ function cancelEdit() {
// Désactiver le mode édition // Désactiver le mode édition
isEditMode.value = false; isEditMode.value = false;
} }
// Add this computed property to get the original image URLs
const originalUrls = computed(() => {
return originalPhotos.value.map(photo => photo.originalUrl);
});
// Add this function to handle photo clicks // Add this function to handle photo clicks
function handlePhotoClick(index) { function handlePhotoClick(index) {
selectedPhotoIndex.value = index; selectedPhotoIndex.value = index;
@@ -425,7 +403,8 @@ function handlePhotoClick(index) {
.video-container { .video-container {
position: relative; position: relative;
width: 100%; width: 100%;
padding-top: 31.25%; /* Reduced from 56.25% to make it shorter while maintaining aspect ratio */ padding-top: 31.25%;
/* Reduced from 56.25% to make it shorter while maintaining aspect ratio */
max-height: 40vh; max-height: 40vh;
} }

View File

@@ -1,73 +1,51 @@
<template> <template>
<div class="album-editor"> <div class="album-editor">
<h2 class="text-xl font-semibold mb-4"> <h2 class="mb-4 text-xl font-semibold">
{{ t('title') }} {{ t('title') }}
</h2> </h2>
<!-- Drop zone with photos --> <!-- Drop zone with photos -->
<div class="drop-zone" <div class="drop-zone" @dragover.prevent @drop.prevent="handleDrop" @click="triggerFileInput">
@dragover.prevent
@drop.prevent="handleDrop"
@click="triggerFileInput">
<!-- Upload prompt --> <!-- Upload prompt -->
<div class="drop-zone-content"> <div class="drop-zone-content">
<v-icon size="large">mdi-plus</v-icon> <v-icon size="large">mdi-plus</v-icon>
<span class="text-sm mt-2">{{ t('dropzoneText') }}</span> <span class="mt-2 text-sm">{{ t('dropzoneText') }}</span>
</div> </div>
<!-- Hidden file input --> <!-- Hidden file input -->
<input <input type="file" ref="fileInput" @change="handleFileUpload" accept="image/*" multiple class="hidden" />
type="file"
ref="fileInput"
@change="handleFileUpload"
accept="image/*"
multiple
class="hidden"
/>
<!-- Photos grid --> <!-- Photos grid -->
<draggable <draggable v-model="localImages" class="photos-grid" item-key="id" @end="handleReorder">
v-model="localImages"
class="photos-grid"
item-key="id"
@end="handleReorder"
>
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="photo-wrapper" @click.stop="toggleMobileControls(index)"> <div class="photo-wrapper" @click.stop="toggleMobileControls(index)">
<div class="index-bubble">{{ index + 1 }}</div> <div class="index-bubble">{{ index + 1 }}</div>
<img :src="element.url" :alt="'Image ' + (index + 1)" /> <img :src="element.image.originalUrl" :alt="'Image ' + (index + 1)" />
<!-- Processing spinner overlay --> <!-- Processing spinner overlay -->
<div v-if="element.isProcessing" class="loading-overlay"> <div v-if="element.isProcessing" class="loading-overlay">
<v-progress-circular indeterminate color="primary"></v-progress-circular> <v-progress-circular indeterminate color="primary"></v-progress-circular>
<span class="text-white text-sm mt-2">{{ t('processing') }}</span> <span class="mt-2 text-sm text-white">{{ t('processing') }}</span>
</div> </div>
<!-- Upload spinner overlay --> <!-- Upload spinner overlay -->
<div v-if="element.isUploading" class="loading-overlay uploading"> <div v-if="element.isUploading" class="loading-overlay uploading">
<v-progress-circular indeterminate color="secondary"></v-progress-circular> <v-progress-circular indeterminate color="secondary"></v-progress-circular>
<span class="text-white text-sm mt-2">{{ t('uploading') }}</span> <span class="mt-2 text-sm text-white">{{ t('uploading') }}</span>
</div> </div>
<!-- Left arrow --> <!-- Left arrow -->
<button @click.stop="moveImage(index, 'up')" <button @click.stop="moveImage(index, 'up')" class="action-btn left-btn" :disabled="index === 0"
class="action-btn left-btn" :title="t('moveLeft')" :class="{ 'mobile-active': activePhotoIndex === index }">
:disabled="index === 0"
:title="t('moveLeft')"
:class="{'mobile-active': activePhotoIndex === index}">
<v-icon>mdi-arrow-left</v-icon> <v-icon>mdi-arrow-left</v-icon>
</button> </button>
<!-- Right arrow --> <!-- Right arrow -->
<button @click.stop="moveImage(index, 'down')" <button @click.stop="moveImage(index, 'down')" class="action-btn right-btn"
class="action-btn right-btn" :disabled="index === localImages.length - 1" :title="t('moveRight')"
:disabled="index === localImages.length - 1"
:title="t('moveRight')"
:class="{ 'mobile-active': activePhotoIndex === index }"> :class="{ 'mobile-active': activePhotoIndex === index }">
<v-icon>mdi-arrow-right</v-icon> <v-icon>mdi-arrow-right</v-icon>
</button> </button>
<!-- Delete button --> <!-- Delete button -->
<button @click.stop="deleteImage(index)" <button @click.stop="deleteImage(index)" class="action-btn delete-btn" :title="t('delete')"
class="action-btn delete-btn"
:title="t('delete')"
:class="{ 'mobile-active': activePhotoIndex === index }"> :class="{ 'mobile-active': activePhotoIndex === index }">
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</button> </button>
@@ -80,8 +58,9 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted, watch } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { v7 } from 'uuid';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
const props = defineProps({ const props = defineProps({
@@ -100,24 +79,88 @@ const activePhotoIndex = ref(null); // Track which photo is currently active for
onMounted(() => { onMounted(() => {
// Initialize local images with IDs and states // Initialize local images with IDs and states
localImages.value = props.images.map((url, index) => ({ localImages.value = props.images.map((image) => ({
id: index, image: image,
url: url, file: null,
isProcessing: false, isProcessing: false,
isUploading: false, isUploading: false,
file: null // Store the actual file for upload
})); }));
// Add event listener to close active controls when clicking outside // Add event listener to close active controls when clicking outside
document.addEventListener('click', closeActiveControls); document.addEventListener('click', closeActiveControls);
}); });
// Close active controls when component is unmounted
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', closeActiveControls); document.removeEventListener('click', closeActiveControls);
}); });
// Function to handle mobile control visibility function closeActiveControls() {
activePhotoIndex.value = null;
}
watch(localImages, (newVal) => {
console.log('localImages changed:', newVal);
}, { deep: true });
function handleFiles(files) {
console.log('handleFiles:', files)
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
const reader = new FileReader();
// Create a temporary image object with processing state
const tempImage = {
image: {
id: v7(),
originalUrl: '',
},
file: file,
isProcessing: true,
isUploading: false,
};
localImages.value.push(tempImage);
console.log('Processing image:', tempImage);
reader.onload = (e) => {
console.log('Image loaded:', e);
const index = localImages.value.findIndex(local => local.image.id === tempImage.image.id);
if (index !== -1) {
localImages.value[index].image.originalUrl = e.target.result;
localImages.value[index].isProcessing = false;
emit('update:images', localImages.value);
}
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Error processing image:', error);
}
}
}
}
function handleDrop(event) {
console.log('Drop triggered');
const files = Array.from(event.dataTransfer.files);
handleFiles(files);
}
function triggerFileInput() {
console.log('Input triggered');
fileInput.value.click();
}
function handleFileUpload(event) {
console.log('File input triggered');
const files = Array.from(event.target.files);
handleFiles(files);
event.target.value = '';
}
function handleReorder() {
activePhotoIndex.value = null; // Reset active photo
emit('update:images', localImages.value);
}
function toggleMobileControls(index) { function toggleMobileControls(index) {
// If clicking the same photo, toggle the controls // If clicking the same photo, toggle the controls
if (activePhotoIndex.value === index) { if (activePhotoIndex.value === index) {
@@ -128,78 +171,6 @@ function toggleMobileControls(index) {
} }
} }
// Close active controls when clicking outside
function closeActiveControls() {
activePhotoIndex.value = null;
}
// Trigger file input click
function triggerFileInput() {
fileInput.value.click();
}
// Add drop handler
function handleDrop(event) {
const files = Array.from(event.dataTransfer.files);
handleFiles(files);
}
// Extract file handling logic
function handleFiles(files) {
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
const reader = new FileReader();
// Create a temporary image object with processing state
const tempImage = {
id: Date.now() + Math.random(),
url: '',
isProcessing: true,
isUploading: false,
file: file // Store the file for later upload
};
localImages.value.push(tempImage);
reader.onload = (e) => {
const index = localImages.value.findIndex(img => img.id === tempImage.id);
if (index !== -1) {
localImages.value[index] = {
...tempImage,
url: e.target.result,
isProcessing: false
};
emit('update:images', localImages.value.map(img => img.url));
}
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Error processing image:', error);
}
}
}
}
// Update file upload handler to use common function
function handleFileUpload(event) {
const files = Array.from(event.target.files);
handleFiles(files);
event.target.value = '';
}
// Delete an image
function deleteImage(index) {
localImages.value.splice(index, 1);
activePhotoIndex.value = null; // Reset active photo
emit('update:images', localImages.value.map(img => img.url));
}
// Handle reorder after drag and drop
function handleReorder() {
activePhotoIndex.value = null; // Reset active photo
emit('update:images', localImages.value.map(img => img.url));
}
// Add back the moveImage function
function moveImage(index, direction) { function moveImage(index, direction) {
const newIndex = direction === 'up' ? index - 1 : index + 1; const newIndex = direction === 'up' ? index - 1 : index + 1;
if (newIndex >= 0 && newIndex < localImages.value.length) { if (newIndex >= 0 && newIndex < localImages.value.length) {
@@ -207,10 +178,16 @@ function moveImage(index, direction) {
localImages.value[index] = localImages.value[newIndex]; localImages.value[index] = localImages.value[newIndex];
localImages.value[newIndex] = temp; localImages.value[newIndex] = temp;
activePhotoIndex.value = newIndex; // Keep the moved image active activePhotoIndex.value = newIndex; // Keep the moved image active
emit('update:images', localImages.value.map(img => img.url)); emit('update:images', localImages.value);
} }
} }
function deleteImage(index) {
localImages.value.splice(index, 1);
activePhotoIndex.value = null; // Reset active photo
emit('update:images', localImages.value);
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,4 +1,4 @@
<template> <template>
<div class="creator-home"> <div class="creator-home">
<!-- Content sections container --> <!-- Content sections container -->
@@ -6,12 +6,9 @@
<!-- Donation Section --> <!-- Donation Section -->
<div v-if="brandingStore.value?.acceptDonation" class="section sm:hidden"> <div v-if="brandingStore.value?.acceptDonation" class="section sm:hidden">
<DonationButton <DonationButton :creator-id="brandingStore.value?.id" :creator-name="brandingStore.value?.name"
:creator-id="brandingStore.value?.id"
:creator-name="brandingStore.value?.name"
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id" :on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id" :on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id" />
/>
</div> </div>
<!-- About Creator Section --> <!-- About Creator Section -->
@@ -61,7 +58,6 @@ const baseURL = window.location.origin;
mask-composite: exclude; mask-composite: exclude;
pointer-events: none; pointer-events: none;
} }
</style> </style>
<i18n> <i18n>