chore(photos): simplify photo components

This commit is contained in:
2025-05-13 12:59:01 -04:00
parent b36084e090
commit 0733a0ec1c
3 changed files with 85 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
<template> <template>
<div class="p-4 relative" <div class="p-4 relative"
@mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id" @mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id"
@mouseleave="showEditButtons = false"> @mouseleave="showEditButtons = false">
@@ -46,7 +46,7 @@
{{ t('creator.sections.about.title') }} {{ t('creator.sections.about.title') }}
</h1> </h1>
<div> <div>
<!-- Description Section --> <!-- Description Section -->
<div> <div>
<div v-if="!isEditMode"> <div v-if="!isEditMode">
@@ -68,9 +68,9 @@
rows="5" rows="5"
variant="outlined"></v-textarea> 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
@@ -98,18 +98,21 @@
</div> </div>
<!-- Photos Section using Album component --> <!-- Photos Section using Album component -->
<Album <div>
v-if="hasImages || isEditMode" <!-- Use AlbumView for display mode -->
:is-edit-mode="isEditMode" <AlbumView v-if="!isEditMode && hasImages"
:images="imageUrls" :images="imageUrls"
@update:images="updateImages" :class="['content-section', {
@update:isEditMode="isEditMode = $event" 'rounded-b-xl': videoUrl && !isEditMode,
:class="['content-section', { 'rounded-xl': !videoUrl && !isEditMode
'rounded-b-xl': videoUrl && !isEditMode, }]"/>
'rounded-xl': !videoUrl && !isEditMode
}]"
/>
<!-- Use AlbumEditor for edit mode -->
<AlbumEditor v-if="isEditMode"
:images="imageUrls"
@update:images="updateImages"/>
</div>
<!-- Contact Information Section --> <!-- Contact Information Section -->
<div v-if="phoneNumber || email" class="contact-info mt-6"> <div v-if="phoneNumber || email" class="contact-info mt-6">
<!-- Phone Number --> <!-- Phone Number -->
@@ -135,6 +138,8 @@ import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import Album from './Album.vue'; import Album from './Album.vue';
import {buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId} from '@/utils/youtube'; import {buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId} from '@/utils/youtube';
import AlbumEditor from "@/views/creators/AlbumEditor.vue";
import AlbumView from "@/views/creators/AlbumView.vue";
const {t} = useI18n(); const {t} = useI18n();
const creatorProfileStore = useCreatorProfileStore(); const creatorProfileStore = useCreatorProfileStore();
@@ -167,17 +172,17 @@ const canSave = computed(() => {
if (!editableDescription.value || editableDescription.value.trim() === '') { if (!editableDescription.value || editableDescription.value.trim() === '') {
return false; return false;
} }
// Check if description is too long // Check if description is too long
if (editableDescription.value.length > 2000) { if (editableDescription.value.length > 2000) {
return false; return false;
} }
// Check if video URL is invalid (if one is provided) // Check if video URL is invalid (if one is provided)
if (editableVideoUrl.value && !validateVideoUrl(editableVideoUrl.value)) { if (editableVideoUrl.value && !validateVideoUrl(editableVideoUrl.value)) {
return false; return false;
} }
return true; return true;
}); });
@@ -199,12 +204,12 @@ function validateVideoUrl(url) {
videoUrlError.value = ""; videoUrlError.value = "";
return true; return true;
} }
if (!isValidYouTubeUrlOrId(url)) { if (!isValidYouTubeUrlOrId(url)) {
videoUrlError.value = t('creator.validation.invalidYoutubeUrl'); videoUrlError.value = t('creator.validation.invalidYoutubeUrl');
return false; return false;
} }
videoUrlError.value = ""; videoUrlError.value = "";
return true; return true;
} }
@@ -228,13 +233,13 @@ function toggleEditMode() {
// Fetch album data // Fetch album data
async function fetchAlbumData() { async function fetchAlbumData() {
if (!brandingStore.value?.id) return; if (!brandingStore.value?.id) return;
albumId.value = brandingStore.value.id; albumId.value = brandingStore.value.id;
try { try {
// Try to get the album // Try to get the album
const response = await client.get(`/api/albums/${albumId.value}`); const response = await client.get(`/api/albums/${albumId.value}`);
if (response.data && response.data.photos) { if (response.data && response.data.photos) {
// Store original photos for comparison // Store original photos for comparison
originalPhotos.value = response.data.photos; originalPhotos.value = response.data.photos;
@@ -315,7 +320,7 @@ async function saveChanges() {
if (imageUrls.value.length > 0) { if (imageUrls.value.length > 0) {
// Create or update the album // Create or update the album
const albumId = brandingStore.value.id; const albumId = brandingStore.value.id;
try { try {
// Try to create the album first (it will fail if it already exists) // Try to create the album first (it will fail if it already exists)
await client.post('/api/albums', { await client.post('/api/albums', {
@@ -327,13 +332,13 @@ async function saveChanges() {
// Album might already exist, which is fine // Album might already exist, which is fine
console.log("Album might already exist:", error); console.log("Album might already exist:", error);
} }
// Check for deleted photos // Check for deleted photos
const deletedPhotos = originalPhotos.value.filter(originalPhoto => { const deletedPhotos = originalPhotos.value.filter(originalPhoto => {
// If the photo URL is not in the current images array, it was deleted // If the photo URL is not in the current images array, it was deleted
return !imageUrls.value.includes(originalPhoto.photoUrl); return !imageUrls.value.includes(originalPhoto.photoUrl);
}); });
// Delete removed photos // Delete removed photos
for (const photo of deletedPhotos) { for (const photo of deletedPhotos) {
try { try {
@@ -342,7 +347,7 @@ async function saveChanges() {
console.error("Error deleting photo:", error); console.error("Error deleting photo:", error);
} }
} }
// Now add or update photos // Now add or update photos
for (let i = 0; i < imageUrls.value.length; i++) { for (let i = 0; i < imageUrls.value.length; i++) {
const imageUrl = imageUrls.value[i]; const imageUrl = imageUrls.value[i];
@@ -350,14 +355,14 @@ async function saveChanges() {
// 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();
// Convert data URL to file // Convert data URL to file
const response = await fetch(imageUrl); const response = await fetch(imageUrl);
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}.jpg`, {type: 'image/jpeg'});
formData.append('file', file); formData.append('file', file);
await client.post(`/api/albums/${albumId}/photos`, formData, { await client.post(`/api/albums/${albumId}/photos`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
@@ -368,7 +373,7 @@ async function saveChanges() {
}); });
} }
} }
// Refresh album data after changes // Refresh album data after changes
await fetchAlbumData(); await fetchAlbumData();
} }

View File

@@ -2,14 +2,7 @@
<div v-if="hasImages || isEditMode" <div v-if="hasImages || isEditMode"
class="creator-album" class="creator-album"
@click="handleAlbumClick"> @click="handleAlbumClick">
<!-- Use AlbumView for display mode -->
<AlbumView v-if="!isEditMode"
:images="images" />
<!-- Use AlbumEditor for edit mode -->
<AlbumEditor v-if="isEditMode"
:images="images"
@update:images="updateImages" />
</div> </div>
</template> </template>

View File

@@ -35,7 +35,7 @@
@end="handleReorder" @end="handleReorder"
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="photo-wrapper"> <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.url" :alt="'Image ' + (index + 1)" />
<!-- Processing spinner overlay --> <!-- Processing spinner overlay -->
@@ -52,20 +52,23 @@
<button @click.stop="moveImage(index, 'up')" <button @click.stop="moveImage(index, 'up')"
class="action-btn left-btn" class="action-btn left-btn"
:disabled="index === 0" :disabled="index === 0"
:title="t('moveUp')"> :title="t('moveUp')"
: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" :disabled="index === localImages.length - 1"
:title="t('moveDown')"> :title="t('moveDown')"
: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" class="action-btn delete-btn"
:title="t('delete')"> :title="t('delete')"
:class="{'mobile-active': activePhotoIndex === index}">
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</button> </button>
</div> </div>
@@ -77,7 +80,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
@@ -93,6 +96,7 @@ const emit = defineEmits(['update:images']);
const { t } = useI18n(); const { t } = useI18n();
const fileInput = ref(null); const fileInput = ref(null);
const localImages = ref([]); const localImages = ref([]);
const activePhotoIndex = ref(null); // Track which photo is currently active for mobile
onMounted(() => { onMounted(() => {
// Initialize local images with IDs and states // Initialize local images with IDs and states
@@ -103,8 +107,32 @@ onMounted(() => {
isUploading: false, isUploading: false,
file: null // Store the actual file for upload file: null // Store the actual file for upload
})); }));
// Add event listener to close active controls when clicking outside
document.addEventListener('click', closeActiveControls);
}); });
// Close active controls when component is unmounted
onUnmounted(() => {
document.removeEventListener('click', closeActiveControls);
});
// Function to handle mobile control visibility
function toggleMobileControls(index) {
// If clicking the same photo, toggle the controls
if (activePhotoIndex.value === index) {
activePhotoIndex.value = null;
} else {
// Otherwise, set this photo as active
activePhotoIndex.value = index;
}
}
// Close active controls when clicking outside
function closeActiveControls() {
activePhotoIndex.value = null;
}
// Trigger file input click // Trigger file input click
function triggerFileInput() { function triggerFileInput() {
fileInput.value.click(); fileInput.value.click();
@@ -161,11 +189,13 @@ function handleFileUpload(event) {
// Delete an image // Delete an image
function deleteImage(index) { function deleteImage(index) {
localImages.value.splice(index, 1); localImages.value.splice(index, 1);
activePhotoIndex.value = null; // Reset active photo
emit('update:images', localImages.value.map(img => img.url)); emit('update:images', localImages.value.map(img => img.url));
} }
// Handle reorder after drag and drop // Handle reorder after drag and drop
function handleReorder() { function handleReorder() {
activePhotoIndex.value = null; // Reset active photo
emit('update:images', localImages.value.map(img => img.url)); emit('update:images', localImages.value.map(img => img.url));
} }
@@ -176,6 +206,7 @@ function moveImage(index, direction) {
const temp = localImages.value[index]; const temp = localImages.value[index];
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
emit('update:images', localImages.value.map(img => img.url)); emit('update:images', localImages.value.map(img => img.url));
} }
} }
@@ -229,6 +260,7 @@ function moveImage(index, direction) {
@apply rounded-lg; @apply rounded-lg;
@apply overflow-hidden; @apply overflow-hidden;
@apply bg-gray-100; @apply bg-gray-100;
@apply cursor-pointer;
} }
.photo-wrapper img { .photo-wrapper img {
@@ -253,7 +285,15 @@ function moveImage(index, direction) {
@apply z-10; @apply z-10;
} }
.photo-wrapper:hover .action-btn { /* Show buttons on hover for desktop */
@media (hover: hover) {
.photo-wrapper:hover .action-btn {
@apply opacity-100;
}
}
/* For mobile, show buttons when photo is active */
.action-btn.mobile-active {
@apply opacity-100; @apply opacity-100;
} }
@@ -325,52 +365,4 @@ function moveImage(index, direction) {
.loading-overlay.uploading { .loading-overlay.uploading {
@apply bg-opacity-75; @apply bg-opacity-75;
} }
</style> </style>
<i18n>
{
"en": {
"upload": "Upload Photos",
"delete": "Delete",
"dropzoneText": "Click or drag photos here to upload",
"moveUp": "Move Left",
"moveDown": "Move Right",
"uploading": "Uploading...",
"creator": {
"sections": {
"album": {
"title": "Photos"
}
}
}
},
"fr": {
"upload": "Télécharger des photos",
"delete": "Supprimer",
"dropzoneText": "Cliquez ou glissez-déposez les photos ici",
"moveUp": "Déplacer à gauche",
"moveDown": "Déplacer à droite",
"creator": {
"sections": {
"album": {
"title": "Photos"
}
}
}
},
"es": {
"upload": "Subir fotos",
"delete": "Eliminar",
"dropzoneText": "Haga clic o arrastre las fotos aquí",
"moveUp": "Mover a la izquierda",
"moveDown": "Mover a la derecha",
"creator": {
"sections": {
"album": {
"title": "Fotos"
}
}
}
}
}
</i18n>