chore(photos): simplify photo components
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
Reference in New Issue
Block a user