fix(album): improving the creation/update workflow

This commit is contained in:
2025-06-03 15:43:33 -04:00
parent ebb44adba6
commit 4301358d07
5 changed files with 366 additions and 473 deletions

View File

@@ -1,102 +1,93 @@
import axios from 'axios'; import axios from 'axios';
import {useAuthStore} from '@/stores/authStore.js'; import { useAuthStore } from '@/stores/authStore.js';
export function useClient() { export function useClient() {
if (!import.meta.env.VITE_API_URL) { if (!import.meta.env.VITE_API_URL) {
throw new Error('VITE_API_URL is not provided'); throw new Error('VITE_API_URL is not provided');
}
const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'Content-Type': 'application/json'
}
});
const authStore = useAuthStore();
// Request interceptor
client.interceptors.request.use(async (config) => {
// Check if this is the refresh token endpoint
const isRefreshEndpoint = config.url?.includes('api/users/refresh');
// Check if we need to refresh the token
if (authStore.isAuthenticated && !isRefreshEndpoint) {
try {
// If token is expiring soon, start a refresh and WAIT for it to complete
if (authStore.isTokenExpiringSoon(authStore.accessToken)) {
console.log(`Token is expiring soon, waiting for refresh to complete before continuing request: ${config.method?.toUpperCase()} ${config.url}`);
await authStore.refresh();
console.log(`Token refresh completed, proceeding with request: ${config.method?.toUpperCase()} ${config.url}`);
}
} catch (error) {
console.error(`Failed to refresh token for: ${config.method?.toUpperCase()} ${config.url}`, error);
throw error; // This will cancel the request
}
} }
const client = axios.create({ if (authStore.isAuthenticated && !isRefreshEndpoint) {
baseURL: import.meta.env.VITE_API_URL, config.headers.Authorization = `Bearer ${authStore.accessToken}`;
headers: { }
'Content-Type': 'application/json'
if (config.data instanceof FormData) {
console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`);
delete config.headers['Content-Type'];
}
return config;
});
// Response interceptor
client.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
console.error(`Response interceptor caught an error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`, error);
// Prevent retry loops by checking if this is already a retry or a refresh request
if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url.includes('api/users/refresh')) {
console.log(`Received 401 error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, attempting token refresh...`);
originalRequest._retry = true;
try {
// Use a timeout to prevent hanging indefinitely
const refreshTimeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Token refresh timeout')), 10000); // 10s timeout
});
// Race the refresh against the timeout
await Promise.race([authStore.refresh(), refreshTimeout]);
console.log(`Token refresh successful, retrying original request: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
return client(originalRequest);
} catch (refreshError) {
console.error(`Token refresh failed for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, logging out user:`, refreshError);
// Let the authStore handle the navigation with returnUrl
throw refreshError;
} }
}); }
const authStore = useAuthStore(); // If it's a refresh request that failed, or a retry that still failed, give up
if (originalRequest.url.includes('api/users/refresh') || originalRequest._retry) {
console.log(`Request permanently failed: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
}
// Request interceptor return Promise.reject(error);
client.interceptors.request.use(async (config) => { }
console.log(`Request interceptor triggered for: ${config.method?.toUpperCase()} ${config.url}`); );
// Check if this is the refresh token endpoint return client;
const isRefreshEndpoint = config.url?.includes('api/users/refresh'); }
// Check if we need to refresh the token
if (authStore.isAuthenticated && !isRefreshEndpoint) {
try {
console.log(`User is authenticated, checking token for: ${config.method?.toUpperCase()} ${config.url}`);
// If token is expiring soon, start a refresh and WAIT for it to complete
if (authStore.isTokenExpiringSoon(authStore.accessToken)) {
console.log(`Token is expiring soon, waiting for refresh to complete before continuing request: ${config.method?.toUpperCase()} ${config.url}`);
await authStore.refresh();
console.log(`Token refresh completed, proceeding with request: ${config.method?.toUpperCase()} ${config.url}`);
}
} catch (error) {
console.error(`Failed to refresh token for: ${config.method?.toUpperCase()} ${config.url}`, error);
throw error; // This will cancel the request
}
}
if (authStore.isAuthenticated && !isRefreshEndpoint) {
console.log(`Setting Authorization header for: ${config.method?.toUpperCase()} ${config.url}`);
config.headers.Authorization = `Bearer ${authStore.accessToken}`;
} else if (isRefreshEndpoint) {
console.log(`Skipping Authorization header for refresh endpoint: ${config.method?.toUpperCase()} ${config.url}`);
}
if (config.data instanceof FormData) {
console.log(`Data is FormData, removing explicit Content-Type header for: ${config.method?.toUpperCase()} ${config.url}`);
delete config.headers['Content-Type'];
}
return config;
});
// Response interceptor
client.interceptors.response.use(
(response) => {
console.log(`Response received successfully for: ${response.config.method?.toUpperCase()} ${response.config.url}`);
return response;
},
async (error) => {
const originalRequest = error.config;
console.error(`Response interceptor caught an error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`, error);
// Prevent retry loops by checking if this is already a retry or a refresh request
if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url.includes('api/users/refresh')) {
console.log(`Received 401 error for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, attempting token refresh...`);
originalRequest._retry = true;
try {
// Use a timeout to prevent hanging indefinitely
const refreshTimeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Token refresh timeout')), 10000); // 10s timeout
});
// Race the refresh against the timeout
await Promise.race([authStore.refresh(), refreshTimeout]);
console.log(`Token refresh successful, retrying original request: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
return client(originalRequest);
} catch (refreshError) {
console.error(`Token refresh failed for: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}, logging out user:`, refreshError);
// Let the authStore handle the navigation with returnUrl
throw refreshError;
}
}
// If it's a refresh request that failed, or a retry that still failed, give up
if (originalRequest.url.includes('api/users/refresh') || originalRequest._retry) {
console.log(`Request permanently failed: ${originalRequest.method?.toUpperCase()} ${originalRequest.url}`);
// Don't do anything here, let the refresh error handling work
}
return Promise.reject(error);
}
);
return client;
}

View File

@@ -1,284 +1,283 @@
import {defineStore} from 'pinia'; import { defineStore } from 'pinia';
import {computed, ref} from 'vue'; import { computed, ref } from 'vue';
import {useRouter} from 'vue-router'; import { useRouter } from 'vue-router';
import {useClient} from '@/plugins/api.js'; import { useClient } from '@/plugins/api.js';
import {useSessionStorage} from '@vueuse/core'; import { useSessionStorage } from '@vueuse/core';
import {jwtDecode} from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
import {formatDuration} from "@/internal_time_ago.js"; import { formatDuration } from "@/internal_time_ago.js";
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const clientApi = useClient(); const clientApi = useClient();
const router = useRouter(); const router = useRouter();
const isRefreshing = ref(false); const isRefreshing = ref(false);
let refreshPromise = null; let refreshPromise = null;
const accessToken = useSessionStorage('auth-accessToken', undefined); const accessToken = useSessionStorage('auth-accessToken', undefined);
const refreshToken = useSessionStorage('auth-refreshToken', undefined); const refreshToken = useSessionStorage('auth-refreshToken', undefined);
const tokenClaims = useSessionStorage('auth-tokenClaims', null, { const tokenClaims = useSessionStorage('auth-tokenClaims', null, {
serializer: { serializer: {
read: (v) => (v ? JSON.parse(v) : null), read: (v) => (v ? JSON.parse(v) : null),
write: (v) => (v ? JSON.stringify(v) : null) write: (v) => (v ? JSON.stringify(v) : null)
} }
}); });
const isAuthenticated = computed(() => !!accessToken.value); const isAuthenticated = computed(() => !!accessToken.value);
const userId = computed(() => tokenClaims.value?.sub); const userId = computed(() => tokenClaims.value?.sub);
function updateTokens(data) { function updateTokens(data) {
console.log('updateTokens called with response data:', data); if (!data?.accessToken || !data?.refreshToken) {
if (!data?.accessToken || !data?.refreshToken) { throw new Error('Invalid token data');
throw new Error('Invalid token data'); }
} accessToken.value = data.accessToken;
accessToken.value = data.accessToken; refreshToken.value = data.refreshToken;
refreshToken.value = data.refreshToken; const claims = getClaimsFromToken(data.accessToken);
const claims = getClaimsFromToken(data.accessToken); tokenClaims.value = claims;
tokenClaims.value = claims; console.log('Tokens updated, user ID:', claims?.sub);
console.log('Tokens updated, user ID:', claims?.sub); }
function cleanTokens() {
console.log('cleanTokens called - clearing stored tokens');
accessToken.value = undefined;
refreshToken.value = undefined;
tokenClaims.value = null;
}
async function logout(redirectTo = '/landing') {
console.log('logout called, redirecting to:', redirectTo);
try {
// Optionally call logout endpoint if you have one
// await clientApi.post('api/users/logout');
} catch (error) {
console.error('Logout failed:', error);
} finally {
cleanTokens();
await router.push(redirectTo);
}
}
async function login(email, password) {
console.log('login called with email:', email);
if (!email || !password) {
throw new Error('Email and password are required');
} }
function cleanTokens() { try {
console.log('cleanTokens called - clearing stored tokens'); const response = await clientApi.post('api/users/login', {
accessToken.value = undefined; email: email.trim(),
refreshToken.value = undefined; password: password
tokenClaims.value = null; });
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid login response');
}
updateTokens(response.data);
console.log('login successful');
return true;
} catch (error) {
console.error('Login failed:', error);
cleanTokens();
throw error;
}
}
async function loginWithGoogle(accessTokenParam) {
console.log('loginWithGoogle called');
if (!accessTokenParam) {
throw new Error('Google access token is required');
} }
async function logout(redirectTo = '/landing') { try {
console.log('logout called, redirecting to:', redirectTo); const response = await clientApi.post('api/users/login-with-google', {
token: accessTokenParam
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Google login response');
}
updateTokens(response.data);
console.log('Google login successful');
return true;
} catch (error) {
console.error('Google login failed:', error);
cleanTokens();
throw error;
}
}
async function loginWithFacebook(authResponse) {
console.log('loginWithFacebook called');
if (!authResponse?.accessToken) {
throw new Error('Facebook access token is required');
}
try {
const response = await clientApi.post('api/users/login-with-facebook', {
token: authResponse.accessToken
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Facebook login response');
}
updateTokens(response.data);
console.log('Facebook login successful');
return true;
} catch (error) {
console.error('Facebook login failed:', error);
cleanTokens();
throw error;
}
}
async function refresh() {
console.log('refresh called');
if (!refreshToken.value) {
cleanTokens(); // Clear tokens first
throw new Error('No refresh token available');
}
if (isRefreshing.value && refreshPromise) {
console.log('Already refreshing, returning existing refreshPromise');
return refreshPromise;
}
try {
isRefreshing.value = true;
refreshPromise = (async () => {
try { try {
// Optionally call logout endpoint if you have one console.log('Sending refresh request...');
// await clientApi.post('api/users/logout');
const response = await clientApi.post('api/users/refresh', {
refreshToken: refreshToken.value
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid refresh response');
}
updateTokens({
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
});
console.log('Token refresh successful');
return true;
} catch (error) { } catch (error) {
console.error('Logout failed:', error); console.error('Token refresh failed:', error);
} finally { cleanTokens();
cleanTokens();
await router.push(redirectTo); const currentRoute = router.currentRoute.value;
const returnUrl = currentRoute.fullPath;
// Handle navigation
router.push({
name: 'login',
query: { returnUrl }
}).catch(navError => {
console.error('Navigation error after token refresh failure:', navError);
});
throw error; // Re-throw to notify callers
} }
})();
return await refreshPromise;
} catch (error) {
throw error;
} finally {
// Ensure these are always reset, even if an error is thrown
isRefreshing.value = false;
refreshPromise = null;
}
}
function getClaimsFromToken(token) {
if (!token) return null;
try {
return jwtDecode(token);
} catch (error) {
console.error('Failed to decode token:', error);
return null;
}
}
function isTokenExpiringSoon(token) {
if (!token) {
console.log('No token provided, considered expiring soon');
return true;
} }
async function login(email, password) { const claims = getClaimsFromToken(token);
console.log('login called with email:', email); if (!claims || !claims.exp) {
if (!email || !password) { console.log('No valid claims found, considered expiring soon');
throw new Error('Email and password are required'); return true;
}
try {
const response = await clientApi.post('api/users/login', {
email: email.trim(),
password: password
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid login response');
}
updateTokens(response.data);
console.log('login successful');
return true;
} catch (error) {
console.error('Login failed:', error);
cleanTokens();
throw error;
}
} }
async function loginWithGoogle(accessTokenParam) { const expirationTime = claims.exp * 1000; // Convert to milliseconds
console.log('loginWithGoogle called'); const currentTime = Date.now();
if (!accessTokenParam) { const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
throw new Error('Google access token is required');
}
try { // Calculate time remaining (can be negative if already expired)
const response = await clientApi.post('api/users/login-with-google', { const timeRemainingMs = expirationTime - currentTime;
token: accessTokenParam
});
if (!response.data?.accessToken || !response.data?.refreshToken) { // Token is expiring soon if less than 2 minutes remaining or already expired
throw new Error('Invalid Google login response'); const isExpiring = timeRemainingMs < fiveMinutesInMs;
}
updateTokens(response.data); // Determine the sign for display purposes
console.log('Google login successful'); const formattedTimeRemaining = timeRemainingMs < 0
return true; ? `-${formatDuration(Math.abs(timeRemainingMs))}`
} catch (error) { : formatDuration(timeRemainingMs);
console.error('Google login failed:', error);
cleanTokens(); if (isExpiring) {
throw error; console.log(`Token expiration check; is token expired: ${isExpiring}`, {
} expirationTime: new Date(expirationTime).toLocaleString(),
currentTime: new Date(currentTime).toLocaleString(),
timeRemaining: formattedTimeRemaining
});
} }
async function loginWithFacebook(authResponse) { return isExpiring;
console.log('loginWithFacebook called'); }
if (!authResponse?.accessToken) {
throw new Error('Facebook access token is required');
}
try { async function changePassword(newPassword) {
const response = await clientApi.post('api/users/login-with-facebook', { console.log('changePassword called');
token: authResponse.accessToken if (!isAuthenticated.value) {
}); throw new Error('User must be authenticated to change password');
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid Facebook login response');
}
updateTokens(response.data);
console.log('Facebook login successful');
return true;
} catch (error) {
console.error('Facebook login failed:', error);
cleanTokens();
throw error;
}
} }
async function refresh() { if (!newPassword) {
console.log('refresh called'); throw new Error('New password is required');
if (!refreshToken.value) {
cleanTokens(); // Clear tokens first
throw new Error('No refresh token available');
}
if (isRefreshing.value && refreshPromise) {
console.log('Already refreshing, returning existing refreshPromise');
return refreshPromise;
}
try {
isRefreshing.value = true;
refreshPromise = (async () => {
try {
console.log('Sending refresh request...');
const response = await clientApi.post('api/users/refresh', {
refreshToken: refreshToken.value
});
if (!response.data?.accessToken || !response.data?.refreshToken) {
throw new Error('Invalid refresh response');
}
updateTokens({
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
});
console.log('Token refresh successful');
return true;
} catch (error) {
console.error('Token refresh failed:', error);
cleanTokens();
const currentRoute = router.currentRoute.value;
const returnUrl = currentRoute.fullPath;
// Handle navigation
router.push({
name: 'login',
query: {returnUrl}
}).catch(navError => {
console.error('Navigation error after token refresh failure:', navError);
});
throw error; // Re-throw to notify callers
}
})();
return await refreshPromise;
} catch (error) {
throw error;
} finally {
// Ensure these are always reset, even if an error is thrown
isRefreshing.value = false;
refreshPromise = null;
}
} }
function getClaimsFromToken(token) { try {
if (!token) return null; const response = await clientApi.post('api/users/set-password', {
try { newPassword
return jwtDecode(token); });
} catch (error) {
console.error('Failed to decode token:', error); console.log('Password changed successfully');
return null; return true;
} } catch (error) {
console.error('Password change failed:', error);
throw error;
} }
}
function isTokenExpiringSoon(token) { return {
console.log('isTokenExpiringSoon called'); accessToken,
refreshToken,
if (!token) { isAuthenticated,
console.log('No token provided, considered expiring soon'); userId,
return true; isRefreshing,
} login,
loginWithGoogle,
const claims = getClaimsFromToken(token); loginWithFacebook,
if (!claims || !claims.exp) { logout,
console.log('No valid claims found, considered expiring soon'); refresh,
return true; isTokenExpiringSoon,
} changePassword
};
const expirationTime = claims.exp * 1000; // Convert to milliseconds });
const currentTime = Date.now();
const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration
// Calculate time remaining (can be negative if already expired)
const timeRemainingMs = expirationTime - currentTime;
// Token is expiring soon if less than 2 minutes remaining or already expired
const isExpiring = timeRemainingMs < fiveMinutesInMs;
// Determine the sign for display purposes
const formattedTimeRemaining = timeRemainingMs < 0
? `-${formatDuration(Math.abs(timeRemainingMs))}`
: formatDuration(timeRemainingMs);
console.log(`Token expiration check; is token expired: ${isExpiring}`, {
expirationTime: new Date(expirationTime).toLocaleString(),
currentTime: new Date(currentTime).toLocaleString(),
timeRemaining: formattedTimeRemaining
});
return isExpiring;
}
async function changePassword(newPassword) {
console.log('changePassword called');
if (!isAuthenticated.value) {
throw new Error('User must be authenticated to change password');
}
if (!newPassword) {
throw new Error('New password is required');
}
try {
const response = await clientApi.post('api/users/set-password', {
newPassword
});
console.log('Password changed successfully');
return true;
} catch (error) {
console.error('Password change failed:', error);
throw error;
}
}
return {
accessToken,
refreshToken,
isAuthenticated,
userId,
isRefreshing,
login,
loginWithGoogle,
loginWithFacebook,
logout,
refresh,
isTokenExpiringSoon,
changePassword
};
});

View File

@@ -214,11 +214,11 @@ async function fetchAlbumData() {
console.log('in fetchAlbumData()'); console.log('in fetchAlbumData()');
if (!brandingStore.value?.id) return; if (!brandingStore.value?.id) return;
albumId.value = brandingStore.value.id; const creatorId = 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/${creatorId}`);
if (response.data && response.data.photos) { if (response.data && response.data.photos) {
// Store original photos for comparison // Store original photos for comparison
@@ -230,15 +230,15 @@ async function fetchAlbumData() {
isProcessing: false, isProcessing: false,
isUploading: false, isUploading: false,
})); }));
albumId.value = creatorId;
} else { } else {
// Initialize with empty array instead of empty slots // Initialize with empty array instead of empty slots
console.log('WOW! You found how to get here! Take a look at the stack!');
photos.value = []; photos.value = [];
originalPhotos.value = []; originalPhotos.value = [];
} }
} catch (error) { }
// Album might not exist yet, which is fine catch (error) {
console.log("Album might not exist yet:", error);
// Initialize with empty array instead of empty slots
photos.value = []; photos.value = [];
originalPhotos.value = []; originalPhotos.value = [];
} }
@@ -302,46 +302,48 @@ async function saveChanges() {
videoUrl.value = extractVideoId(editableVideoUrl.value) || ""; videoUrl.value = extractVideoId(editableVideoUrl.value) || "";
// Check for deleted photos // Check for deleted photos
console.log('originalPhotos', originalPhotos.value);
console.log('photos', photos.value);
const photosOriginalUrls = photos.value.map(photo => photo.image.originalUrl); const photosOriginalUrls = photos.value.map(photo => photo.image.originalUrl);
console.log('photosThumbnailUrls', photosOriginalUrls);
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 !photosOriginalUrls.includes(originalPhoto.originalUrl); return !photosOriginalUrls.includes(originalPhoto.originalUrl);
}); });
const newImages = photos.value.filter(photo => photo && photo.image && photo.image.originalUrl.startsWith('data:'));
console.log('originalPhotos', originalPhotos.value);
console.log('photos', photos.value);
console.log('deletedPhotos', deletedPhotos); console.log('deletedPhotos', deletedPhotos);
console.log('newImages', newImages);
// Save album photos if they've changed // Save album photos if they've changed
if (photos.value.length > 0 || deletedPhotos.length > 0) { if (photos.value.length > 0 || deletedPhotos.length > 0) {
// Create or update the album console.log('We got pending changes');
const albumId = brandingStore.value.id;
try { // Create the Album if we do not have one yet
// Try to create the album first (it will fail if it already exists) if (albumId.value == null) {
await client.post('/api/albums', { console.log('We do not have an album yet')
albumId: albumId, try {
title: `${brandingStore.value.name}'s Album`, await client.post('/api/albums', {
description: "Photo album for the creator" albumId: brandingStore.value.id,
}); title: `${brandingStore.value.name}'s Album`,
} catch (error) { description: "Photo album for the creator"
// Album might already exist, which is fine });
console.log("Album might already exist:", error); albumId.value = brandingStore.value.id;
} catch (error) {
// Album might already exist, which is fine
console.log("Couldn't create an Album", error);
}
} }
// Delete removed photos // Delete removed photos
for (const photo of deletedPhotos) { for (const photo of deletedPhotos) {
try { try {
await client.delete(`/api/albums/${albumId}/photos/${photo.id}`); await client.delete(`/api/albums/${albumId.value}/photos/${photo.id}`);
} catch (error) { } catch (error) {
console.error("Error deleting photo:", error); console.error("Error deleting photo:", error);
} }
} }
// Now add or update photos // Now add or update photos
const newImages = photos.value.filter(photo => photo && photo.image && photo.image.originalUrl.startsWith('data:'));
console.log('newImages', newImages);
for (let i = 0; i < newImages.length; i++) { for (let i = 0; i < newImages.length; i++) {
const imageData = newImages[i]; const imageData = newImages[i];
console.log('Image Data to be uploaded:', imageData); console.log('Image Data to be uploaded:', imageData);
@@ -356,7 +358,7 @@ async function saveChanges() {
formData.append('file', file); formData.append('file', file);
await client.post(`/api/albums/${albumId}/photos`, formData, { await client.post(`/api/albums/${albumId.value}/photos`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
}, },

View File

@@ -1,95 +0,0 @@
<template>
<div v-if="hasImages || isEditMode"
class="creator-album"
@click="handleAlbumClick">
</div>
</template>
<script setup>
import { computed } from "vue";
import AlbumView from './AlbumView.vue';
import AlbumEditor from './AlbumEditor.vue';
const props = defineProps({
isEditMode: {
type: Boolean,
required: true
},
images: {
type: Array,
required: true,
default: () => []
}
});
const emit = defineEmits(['update:images']);
// Computed property to check if there are images
const hasImages = computed(() => {
return props.images.some(url => url);
});
// Handle album click to enter edit mode
function handleAlbumClick() {
if (!props.isEditMode) {
emit('update:isEditMode', true);
}
}
// Update images from AlbumEditor component
function updateImages(newImages) {
emit('update:images', newImages);
}
</script>
<style scoped>
.creator-album {
@apply w-full;
cursor: pointer;
}
</style>
<i18n>
{
"en": {
"common": {
"delete": "Delete"
},
"creator": {
"sections": {
"album": {
"title": "Photo Album",
"image": "Album image"
}
}
}
},
"fr": {
"common": {
"delete": "Supprimer"
},
"creator": {
"sections": {
"album": {
"title": "Album photo",
"image": "Image de l'album"
}
}
}
},
"es": {
"common": {
"delete": "Eliminar"
},
"creator": {
"sections": {
"album": {
"title": "Álbum de fotos",
"image": "Imagen del álbum"
}
}
}
}
}
</i18n>

View File

@@ -58,7 +58,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch } from "vue"; import { ref, onMounted } from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { v7 } from 'uuid'; import { v7 } from 'uuid';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
@@ -81,10 +81,6 @@ onMounted(() => {
localImages.value = props.images; localImages.value = props.images;
}); });
watch(localImages, (newVal) => {
console.log('localImages changed:', newVal);
}, { deep: true });
function handleFiles(files) { function handleFiles(files) {
console.log('handleFiles:', files) console.log('handleFiles:', files)
for (const file of files) { for (const file of files) {