diff --git a/frontend/src/plugins/api.js b/frontend/src/plugins/api.js index 2f9dcef..e0c564f 100644 --- a/frontend/src/plugins/api.js +++ b/frontend/src/plugins/api.js @@ -1,102 +1,93 @@ import axios from 'axios'; -import {useAuthStore} from '@/stores/authStore.js'; +import { useAuthStore } from '@/stores/authStore.js'; export function useClient() { - if (!import.meta.env.VITE_API_URL) { - throw new Error('VITE_API_URL is not provided'); + if (!import.meta.env.VITE_API_URL) { + 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({ - baseURL: import.meta.env.VITE_API_URL, - headers: { - 'Content-Type': 'application/json' + if (authStore.isAuthenticated && !isRefreshEndpoint) { + config.headers.Authorization = `Bearer ${authStore.accessToken}`; + } + + 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 - client.interceptors.request.use(async (config) => { - console.log(`Request interceptor triggered for: ${config.method?.toUpperCase()} ${config.url}`); + return Promise.reject(error); + } + ); - // 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 { - 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; -} \ No newline at end of file + return client; +} diff --git a/frontend/src/stores/authStore.js b/frontend/src/stores/authStore.js index 3841156..1b1663e 100644 --- a/frontend/src/stores/authStore.js +++ b/frontend/src/stores/authStore.js @@ -1,284 +1,283 @@ -import {defineStore} from 'pinia'; -import {computed, ref} from 'vue'; -import {useRouter} from 'vue-router'; -import {useClient} from '@/plugins/api.js'; -import {useSessionStorage} from '@vueuse/core'; -import {jwtDecode} from 'jwt-decode'; -import {formatDuration} from "@/internal_time_ago.js"; +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; +import { useRouter } from 'vue-router'; +import { useClient } from '@/plugins/api.js'; +import { useSessionStorage } from '@vueuse/core'; +import { jwtDecode } from 'jwt-decode'; +import { formatDuration } from "@/internal_time_ago.js"; export const useAuthStore = defineStore('auth', () => { - const clientApi = useClient(); - const router = useRouter(); + const clientApi = useClient(); + const router = useRouter(); - const isRefreshing = ref(false); - let refreshPromise = null; + const isRefreshing = ref(false); + let refreshPromise = null; - const accessToken = useSessionStorage('auth-accessToken', undefined); - const refreshToken = useSessionStorage('auth-refreshToken', undefined); - const tokenClaims = useSessionStorage('auth-tokenClaims', null, { - serializer: { - read: (v) => (v ? JSON.parse(v) : null), - write: (v) => (v ? JSON.stringify(v) : null) - } - }); + const accessToken = useSessionStorage('auth-accessToken', undefined); + const refreshToken = useSessionStorage('auth-refreshToken', undefined); + const tokenClaims = useSessionStorage('auth-tokenClaims', null, { + serializer: { + read: (v) => (v ? JSON.parse(v) : null), + write: (v) => (v ? JSON.stringify(v) : null) + } + }); - const isAuthenticated = computed(() => !!accessToken.value); - const userId = computed(() => tokenClaims.value?.sub); + const isAuthenticated = computed(() => !!accessToken.value); + const userId = computed(() => tokenClaims.value?.sub); - function updateTokens(data) { - console.log('updateTokens called with response data:', data); - if (!data?.accessToken || !data?.refreshToken) { - throw new Error('Invalid token data'); - } - accessToken.value = data.accessToken; - refreshToken.value = data.refreshToken; - const claims = getClaimsFromToken(data.accessToken); - tokenClaims.value = claims; - console.log('Tokens updated, user ID:', claims?.sub); + function updateTokens(data) { + if (!data?.accessToken || !data?.refreshToken) { + throw new Error('Invalid token data'); + } + accessToken.value = data.accessToken; + refreshToken.value = data.refreshToken; + const claims = getClaimsFromToken(data.accessToken); + tokenClaims.value = claims; + 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() { - console.log('cleanTokens called - clearing stored tokens'); - accessToken.value = undefined; - refreshToken.value = undefined; - tokenClaims.value = null; + 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) { + console.log('loginWithGoogle called'); + if (!accessTokenParam) { + throw new Error('Google access token is required'); } - async function logout(redirectTo = '/landing') { - console.log('logout called, redirecting to:', redirectTo); + try { + 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 { - // Optionally call logout endpoint if you have one - // await clientApi.post('api/users/logout'); + 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('Logout failed:', error); - } finally { - cleanTokens(); - await router.push(redirectTo); + 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) { + 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) { - console.log('login called with email:', email); - if (!email || !password) { - throw new Error('Email and password are required'); - } - - 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; - } + const claims = getClaimsFromToken(token); + if (!claims || !claims.exp) { + console.log('No valid claims found, considered expiring soon'); + return true; } - async function loginWithGoogle(accessTokenParam) { - console.log('loginWithGoogle called'); - if (!accessTokenParam) { - throw new Error('Google access token is required'); - } + const expirationTime = claims.exp * 1000; // Convert to milliseconds + const currentTime = Date.now(); + const fiveMinutesInMs = 2 * 60 * 1000; // 2 minutes for demonstration - try { - const response = await clientApi.post('api/users/login-with-google', { - token: accessTokenParam - }); + // Calculate time remaining (can be negative if already expired) + const timeRemainingMs = expirationTime - currentTime; - if (!response.data?.accessToken || !response.data?.refreshToken) { - throw new Error('Invalid Google login response'); - } + // Token is expiring soon if less than 2 minutes remaining or already expired + const isExpiring = timeRemainingMs < fiveMinutesInMs; - updateTokens(response.data); - console.log('Google login successful'); - return true; - } catch (error) { - console.error('Google login failed:', error); - cleanTokens(); - throw error; - } + // Determine the sign for display purposes + const formattedTimeRemaining = timeRemainingMs < 0 + ? `-${formatDuration(Math.abs(timeRemainingMs))}` + : formatDuration(timeRemainingMs); + + if (isExpiring) { + 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) { - console.log('loginWithFacebook called'); - if (!authResponse?.accessToken) { - throw new Error('Facebook access token is required'); - } + return isExpiring; + } - 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 changePassword(newPassword) { + console.log('changePassword called'); + if (!isAuthenticated.value) { + throw new Error('User must be authenticated to change password'); } - 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 { - 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; - } + if (!newPassword) { + throw new Error('New password is required'); } - function getClaimsFromToken(token) { - if (!token) return null; - try { - return jwtDecode(token); - } catch (error) { - console.error('Failed to decode token:', error); - return null; - } + 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; } + } - function isTokenExpiringSoon(token) { - console.log('isTokenExpiringSoon called'); - - if (!token) { - console.log('No token provided, considered expiring soon'); - return true; - } - - const claims = getClaimsFromToken(token); - if (!claims || !claims.exp) { - console.log('No valid claims found, considered expiring soon'); - return true; - } - - 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 - }; -}); \ No newline at end of file + return { + accessToken, + refreshToken, + isAuthenticated, + userId, + isRefreshing, + login, + loginWithGoogle, + loginWithFacebook, + logout, + refresh, + isTokenExpiringSoon, + changePassword + }; +}); diff --git a/frontend/src/views/creators/AboutCreator.vue b/frontend/src/views/creators/AboutCreator.vue index d0526de..cc371b9 100644 --- a/frontend/src/views/creators/AboutCreator.vue +++ b/frontend/src/views/creators/AboutCreator.vue @@ -214,11 +214,11 @@ async function fetchAlbumData() { console.log('in fetchAlbumData()'); if (!brandingStore.value?.id) return; - albumId.value = brandingStore.value.id; + const creatorId = brandingStore.value.id; try { // 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) { // Store original photos for comparison @@ -230,15 +230,15 @@ async function fetchAlbumData() { isProcessing: false, isUploading: false, })); + albumId.value = creatorId; } else { // 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 = []; originalPhotos.value = []; } - } catch (error) { - // Album might not exist yet, which is fine - console.log("Album might not exist yet:", error); - // Initialize with empty array instead of empty slots + } + catch (error) { photos.value = []; originalPhotos.value = []; } @@ -302,46 +302,48 @@ async function saveChanges() { videoUrl.value = extractVideoId(editableVideoUrl.value) || ""; // Check for deleted photos - console.log('originalPhotos', originalPhotos.value); - console.log('photos', photos.value); const photosOriginalUrls = photos.value.map(photo => photo.image.originalUrl); - console.log('photosThumbnailUrls', photosOriginalUrls); const deletedPhotos = originalPhotos.value.filter(originalPhoto => { // If the photo URL is not in the current images array, it was deleted 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('newImages', newImages); // Save album photos if they've changed if (photos.value.length > 0 || deletedPhotos.length > 0) { - // Create or update the album - const albumId = brandingStore.value.id; + console.log('We got pending changes'); - try { - // Try to create the album first (it will fail if it already exists) - await client.post('/api/albums', { - albumId: albumId, - title: `${brandingStore.value.name}'s Album`, - description: "Photo album for the creator" - }); - } catch (error) { - // Album might already exist, which is fine - console.log("Album might already exist:", error); + // Create the Album if we do not have one yet + if (albumId.value == null) { + console.log('We do not have an album yet') + try { + await client.post('/api/albums', { + albumId: brandingStore.value.id, + title: `${brandingStore.value.name}'s Album`, + description: "Photo album for the creator" + }); + 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 for (const photo of deletedPhotos) { try { - await client.delete(`/api/albums/${albumId}/photos/${photo.id}`); + await client.delete(`/api/albums/${albumId.value}/photos/${photo.id}`); } catch (error) { console.error("Error deleting photo:", error); } } // 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++) { const imageData = newImages[i]; console.log('Image Data to be uploaded:', imageData); @@ -356,7 +358,7 @@ async function saveChanges() { formData.append('file', file); - await client.post(`/api/albums/${albumId}/photos`, formData, { + await client.post(`/api/albums/${albumId.value}/photos`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, diff --git a/frontend/src/views/creators/Album.vue b/frontend/src/views/creators/Album.vue deleted file mode 100644 index f2255a6..0000000 --- a/frontend/src/views/creators/Album.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - -{ - "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" - } - } - } - } -} - \ No newline at end of file diff --git a/frontend/src/views/creators/AlbumEditor.vue b/frontend/src/views/creators/AlbumEditor.vue index d26af18..8a5f0e9 100644 --- a/frontend/src/views/creators/AlbumEditor.vue +++ b/frontend/src/views/creators/AlbumEditor.vue @@ -58,7 +58,7 @@