fix(album): removing last album image was not working
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"vue",
|
||||
"tailwindcss"
|
||||
],
|
||||
"rules": {}
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"plugin:tailwindcss/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"vue",
|
||||
"tailwindcss"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
14
frontend/.prettierrc
Normal file
14
frontend/.prettierrc
Normal 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"
|
||||
}
|
||||
138
frontend/package-lock.json
generated
138
frontend/package-lock.json
generated
@@ -32,10 +32,13 @@
|
||||
"devDependencies": {
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"eslint-plugin-vue": "^9.22.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
@@ -975,6 +978,18 @@
|
||||
"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": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||
@@ -1554,6 +1569,20 @@
|
||||
"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": {
|
||||
"version": "3.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz",
|
||||
@@ -2460,6 +2489,67 @@
|
||||
"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": {
|
||||
"version": "9.33.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -4090,6 +4186,33 @@
|
||||
"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": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
@@ -4626,6 +4749,21 @@
|
||||
"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": {
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
|
||||
@@ -33,10 +33,13 @@
|
||||
"devDependencies": {
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"eslint-plugin-vue": "^9.22.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
|
||||
@@ -1,40 +1,27 @@
|
||||
<template>
|
||||
<div class="p-4 relative"
|
||||
@mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id"
|
||||
@mouseleave="showEditButtons = false">
|
||||
<div class="relative p-4"
|
||||
@mouseenter="showEditButtons = isLoggedIn && creatorProfileStore.creator?.id === brandingStore.value.id"
|
||||
@mouseleave="showEditButtons = false">
|
||||
|
||||
<!-- Edit buttons with absolute positioning -->
|
||||
<div v-if="showEditButtons || isEditMode"
|
||||
class="absolute top-4 right-4 flex gap-2">
|
||||
<div v-if="showEditButtons || isEditMode" class="absolute right-4 top-4 flex gap-2">
|
||||
|
||||
<!-- Edit button with pencil icon -->
|
||||
<button
|
||||
v-if="!isEditMode"
|
||||
:title="t('edit')"
|
||||
class="w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
|
||||
@click="toggleEditMode()"
|
||||
>
|
||||
<button v-if="!isEditMode" :title="t('edit')"
|
||||
class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg"
|
||||
@click="toggleEditMode()">
|
||||
<v-icon large>mdi-pencil</v-icon>
|
||||
</button>
|
||||
|
||||
<!-- Save button -->
|
||||
<button
|
||||
v-if="isEditMode"
|
||||
:disabled="!canSave"
|
||||
:title="t('save')"
|
||||
class="w-12 h-12 bg-hutopyPrimary rounded-full flex items-center justify-center shadow-lg"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<button v-if="isEditMode" :disabled="!canSave" :title="t('save')"
|
||||
class="flex size-12 items-center justify-center rounded-full bg-hutopyPrimary shadow-lg" @click="saveChanges()">
|
||||
<v-icon large>mdi-check</v-icon>
|
||||
</button>
|
||||
|
||||
<!-- Cancel button -->
|
||||
<button
|
||||
v-if="isEditMode"
|
||||
:title="t('cancel')"
|
||||
class="w-12 h-12 bg-red-500 rounded-full flex items-center justify-center shadow-lg"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
<button v-if="isEditMode" :title="t('cancel')"
|
||||
class="flex size-12 items-center justify-center rounded-full bg-red-500 shadow-lg" @click="cancelEdit">
|
||||
<v-icon large>mdi-close</v-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -42,7 +29,7 @@
|
||||
<!-- MainPage -->
|
||||
<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') }}
|
||||
</h1>
|
||||
|
||||
@@ -50,72 +37,47 @@
|
||||
<!-- Description Section -->
|
||||
<div>
|
||||
<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 }}
|
||||
</p>
|
||||
</div>
|
||||
<v-textarea v-if="isEditMode"
|
||||
v-model="editableDescription"
|
||||
:counter="2000"
|
||||
:error-messages="descriptionError"
|
||||
:label="t('creator.sections.about.description')"
|
||||
:rules="[
|
||||
v => !!v || t('creator.validation.descriptionRequired'),
|
||||
v => v.length <= 2000 || t('creator.validation.descriptionTooLong')
|
||||
]"
|
||||
auto-grow
|
||||
class="w-full p-2 py-6"
|
||||
rows="5"
|
||||
variant="outlined"></v-textarea>
|
||||
<v-textarea v-if="isEditMode" v-model="editableDescription" :counter="2000" :error-messages="descriptionError"
|
||||
:label="t('creator.sections.about.description')" :rules="[
|
||||
v => !!v || t('creator.validation.descriptionRequired'),
|
||||
v => v.length <= 2000 || t('creator.validation.descriptionTooLong')
|
||||
]" auto-grow class="w-full p-2 py-6" rows="5" variant="outlined"></v-textarea>
|
||||
</div>
|
||||
|
||||
<!-- Video Section -->
|
||||
<div v-if="videoUrl || isEditMode"
|
||||
:class="['content-section', {
|
||||
'rounded-t-xl': hasImages && !isEditMode,
|
||||
'rounded-xl': !hasImages && !isEditMode
|
||||
}]">
|
||||
<div v-if="videoUrl || isEditMode" :class="['content-section', {
|
||||
'rounded-t-xl': hasImages && !isEditMode,
|
||||
'rounded-xl': !hasImages && !isEditMode
|
||||
}]">
|
||||
<div v-if="!isEditMode && videoUrl" class="video-container">
|
||||
<iframe
|
||||
:src="youtubeEmbedUrl"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="video-frame"
|
||||
title="YouTube video player">
|
||||
<iframe :src="youtubeEmbedUrl"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen class="video-frame" title="YouTube video player">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditMode">
|
||||
<v-text-field
|
||||
v-model="editableVideoUrl"
|
||||
:error-messages="videoUrlError"
|
||||
:label="t('creator.fields.videoUrl')"
|
||||
class="w-full p-2"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
/>
|
||||
<v-text-field v-model="editableVideoUrl" :error-messages="videoUrlError"
|
||||
:label="t('creator.fields.videoUrl')" class="w-full p-2" type="text" variant="outlined" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photos Section using Album component -->
|
||||
<div>
|
||||
<!-- Use AlbumView for display mode -->
|
||||
<AlbumView v-if="!isEditMode && hasImages"
|
||||
:class="['content-section', {
|
||||
'rounded-b-xl': videoUrl && !isEditMode,
|
||||
'rounded-xl': !videoUrl && !isEditMode
|
||||
}]"
|
||||
:images="thumbnailUrls"
|
||||
@photo-click="handlePhotoClick"/>
|
||||
<AlbumView v-if="!isEditMode && hasImages" :class="['content-section', {
|
||||
'rounded-b-xl': videoUrl && !isEditMode,
|
||||
'rounded-xl': !videoUrl && !isEditMode
|
||||
}]" :images="thumbnailUrls" @photo-click="handlePhotoClick" />
|
||||
|
||||
<AlbumViewer v-model="showAlbumViewer"
|
||||
:images="originalUrls"
|
||||
:start-index="selectedPhotoIndex"/>
|
||||
<AlbumViewer v-model="showAlbumViewer" :images="originalUrls" :start-index="selectedPhotoIndex" />
|
||||
|
||||
<!-- Use AlbumEditor for edit mode -->
|
||||
<AlbumEditor v-if="isEditMode"
|
||||
:images="thumbnailUrls"
|
||||
@update:images="updateImages"/>
|
||||
<AlbumEditor v-if="isEditMode" :images="photos" @update:images="updateImages" />
|
||||
</div>
|
||||
|
||||
<!-- Contact Information Section -->
|
||||
@@ -136,18 +98,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref, computed, watch} from "vue";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId} from '@/utils/youtube';
|
||||
import { onMounted, ref, computed, watch } from "vue";
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
||||
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { buildEmbedUrl, isValidYouTubeUrlOrId, extractVideoId } from '@/utils/youtube';
|
||||
import AlbumEditor from "@/views/creators/AlbumEditor.vue";
|
||||
import AlbumView from "@/views/creators/AlbumView.vue";
|
||||
// Add these imports at the top with your other imports
|
||||
import AlbumViewer from './AlbumViewer.vue';
|
||||
|
||||
const {t} = useI18n();
|
||||
const { t } = useI18n();
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const brandingStore = useBrandingStore();
|
||||
const client = useClient();
|
||||
@@ -162,7 +123,7 @@ const description = ref("");
|
||||
const videoUrl = ref("");
|
||||
const phoneNumber = ref("");
|
||||
const email = ref("");
|
||||
const thumbnailUrls = ref([]);
|
||||
const photos = ref([]); //before was thumbnailUrls
|
||||
const albumId = ref(null);
|
||||
const originalPhotos = ref([]);
|
||||
// Add these refs with your other refs
|
||||
@@ -195,10 +156,19 @@ const canSave = computed(() => {
|
||||
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
|
||||
const hasImages = computed(() => {
|
||||
// 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
|
||||
@@ -253,17 +223,17 @@ async function fetchAlbumData() {
|
||||
// Store original photos for comparison
|
||||
originalPhotos.value = response.data.photos;
|
||||
// Extract photo URLs from the album photos
|
||||
thumbnailUrls.value = response.data.photos.map(photo => photo.thumbnailUrl);
|
||||
photos.value = response.data.photos;
|
||||
} else {
|
||||
// Initialize with empty array instead of empty slots
|
||||
thumbnailUrls.value = [];
|
||||
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
|
||||
thumbnailUrls.value = [];
|
||||
photos.value = [];
|
||||
originalPhotos.value = [];
|
||||
}
|
||||
}
|
||||
@@ -283,7 +253,7 @@ onMounted(async () => {
|
||||
|
||||
// Update images from Album component
|
||||
function updateImages(newImages) {
|
||||
thumbnailUrls.value = newImages;
|
||||
photos.value = newImages;
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
@@ -314,19 +284,26 @@ async function saveChanges() {
|
||||
|
||||
// Save presentation info
|
||||
const presentationResponse = await client.post(
|
||||
`/api/creators/${brandingStore.value.id}/presentation-infos`,
|
||||
{
|
||||
description: editableDescription.value || "",
|
||||
videoUrl: editableVideoUrl.value || null
|
||||
}
|
||||
`/api/creators/${brandingStore.value.id}/presentation-infos`,
|
||||
{
|
||||
description: editableDescription.value || "",
|
||||
videoUrl: editableVideoUrl.value || null
|
||||
}
|
||||
);
|
||||
|
||||
// Mettre à jour les valeurs locales pour refléter les changements
|
||||
description.value = editableDescription.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
|
||||
if (thumbnailUrls.value.length > 0) {
|
||||
if (photos.value.length > 0 || deletedPhotos.length > 0) {
|
||||
// Create or update the album
|
||||
const albumId = brandingStore.value.id;
|
||||
|
||||
@@ -342,12 +319,6 @@ async function saveChanges() {
|
||||
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
|
||||
for (const photo of deletedPhotos) {
|
||||
try {
|
||||
@@ -358,17 +329,30 @@ async function saveChanges() {
|
||||
}
|
||||
|
||||
// Now add or update photos
|
||||
for (let i = 0; i < thumbnailUrls.value.length; i++) {
|
||||
const imageUrl = thumbnailUrls.value[i];
|
||||
if (imageUrl && imageUrl.startsWith('data:')) {
|
||||
for (let i = 0; i < photos.value.length; i++) {
|
||||
const imageData = photos.value[i];
|
||||
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
|
||||
const photoId = crypto.randomUUID();
|
||||
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
|
||||
const response = await fetch(imageUrl);
|
||||
const response = await fetch(imageData.image.originalUrl);
|
||||
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);
|
||||
|
||||
@@ -404,16 +388,10 @@ function cancelEdit() {
|
||||
// Désactiver le mode édition
|
||||
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
|
||||
function handlePhotoClick(index) {
|
||||
selectedPhotoIndex.value = index;
|
||||
showAlbumViewer.value = true;
|
||||
selectedPhotoIndex.value = index;
|
||||
showAlbumViewer.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -425,7 +403,8 @@ function handlePhotoClick(index) {
|
||||
.video-container {
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +1,52 @@
|
||||
<template>
|
||||
<div class="album-editor">
|
||||
|
||||
<h2 class="text-xl font-semibold mb-4">
|
||||
<h2 class="mb-4 text-xl font-semibold">
|
||||
{{ t('title') }}
|
||||
</h2>
|
||||
|
||||
<!-- Drop zone with photos -->
|
||||
<div class="drop-zone"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
@click="triggerFileInput">
|
||||
<div class="drop-zone" @dragover.prevent @drop.prevent="handleDrop" @click="triggerFileInput">
|
||||
|
||||
<!-- Upload prompt -->
|
||||
<div class="drop-zone-content">
|
||||
<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>
|
||||
|
||||
<!-- Hidden file input -->
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
@change="handleFileUpload"
|
||||
accept="image/*"
|
||||
multiple
|
||||
class="hidden"
|
||||
/>
|
||||
<input type="file" ref="fileInput" @change="handleFileUpload" accept="image/*" multiple class="hidden" />
|
||||
|
||||
<!-- Photos grid -->
|
||||
<draggable
|
||||
v-model="localImages"
|
||||
class="photos-grid"
|
||||
item-key="id"
|
||||
@end="handleReorder"
|
||||
>
|
||||
<draggable v-model="localImages" class="photos-grid" item-key="id" @end="handleReorder">
|
||||
<template #item="{ element, index }">
|
||||
<div class="photo-wrapper" @click.stop="toggleMobileControls(index)">
|
||||
<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 -->
|
||||
<div v-if="element.isProcessing" class="loading-overlay">
|
||||
<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>
|
||||
<!-- Upload spinner overlay -->
|
||||
<div v-if="element.isUploading" class="loading-overlay uploading">
|
||||
<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>
|
||||
<!-- Left arrow -->
|
||||
<button @click.stop="moveImage(index, 'up')"
|
||||
class="action-btn left-btn"
|
||||
:disabled="index === 0"
|
||||
:title="t('moveLeft')"
|
||||
:class="{'mobile-active': activePhotoIndex === index}">
|
||||
<button @click.stop="moveImage(index, 'up')" class="action-btn left-btn" :disabled="index === 0"
|
||||
:title="t('moveLeft')" :class="{ 'mobile-active': activePhotoIndex === index }">
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</button>
|
||||
<!-- Right arrow -->
|
||||
<button @click.stop="moveImage(index, 'down')"
|
||||
class="action-btn right-btn"
|
||||
:disabled="index === localImages.length - 1"
|
||||
:title="t('moveRight')"
|
||||
:class="{'mobile-active': activePhotoIndex === index}">
|
||||
<button @click.stop="moveImage(index, 'down')" class="action-btn right-btn"
|
||||
:disabled="index === localImages.length - 1" :title="t('moveRight')"
|
||||
:class="{ 'mobile-active': activePhotoIndex === index }">
|
||||
<v-icon>mdi-arrow-right</v-icon>
|
||||
</button>
|
||||
<!-- Delete button -->
|
||||
<button @click.stop="deleteImage(index)"
|
||||
class="action-btn delete-btn"
|
||||
:title="t('delete')"
|
||||
:class="{'mobile-active': activePhotoIndex === index}">
|
||||
<button @click.stop="deleteImage(index)" class="action-btn delete-btn" :title="t('delete')"
|
||||
:class="{ 'mobile-active': activePhotoIndex === index }">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -80,8 +58,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { v7 } from 'uuid';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -100,24 +79,88 @@ const activePhotoIndex = ref(null); // Track which photo is currently active for
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize local images with IDs and states
|
||||
localImages.value = props.images.map((url, index) => ({
|
||||
id: index,
|
||||
url: url,
|
||||
localImages.value = props.images.map((image) => ({
|
||||
image: image,
|
||||
file: null,
|
||||
isProcessing: false,
|
||||
isUploading: false,
|
||||
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 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) {
|
||||
// If clicking the same photo, toggle the controls
|
||||
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) {
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (newIndex >= 0 && newIndex < localImages.value.length) {
|
||||
@@ -207,10 +178,16 @@ function moveImage(index, direction) {
|
||||
localImages.value[index] = localImages.value[newIndex];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteImage(index) {
|
||||
localImages.value.splice(index, 1);
|
||||
activePhotoIndex.value = null; // Reset active photo
|
||||
emit('update:images', localImages.value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="creator-home">
|
||||
|
||||
<!-- Content sections container -->
|
||||
@@ -6,17 +6,14 @@
|
||||
|
||||
<!-- Donation Section -->
|
||||
<div v-if="brandingStore.value?.acceptDonation" class="section sm:hidden">
|
||||
<DonationButton
|
||||
:creator-id="brandingStore.value?.id"
|
||||
:creator-name="brandingStore.value?.name"
|
||||
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
||||
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id"
|
||||
/>
|
||||
<DonationButton :creator-id="brandingStore.value?.id" :creator-name="brandingStore.value?.name"
|
||||
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
|
||||
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id" />
|
||||
</div>
|
||||
|
||||
<!-- About Creator Section -->
|
||||
<div class="section">
|
||||
<AboutCreator/>
|
||||
<AboutCreator />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -26,7 +23,7 @@
|
||||
<script setup>
|
||||
import AboutCreator from './AboutCreator.vue';
|
||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
||||
|
||||
const brandingStore = useBrandingStore();
|
||||
const baseURL = window.location.origin;
|
||||
@@ -57,11 +54,10 @@ const baseURL = window.location.origin;
|
||||
@apply p-[1px];
|
||||
background: linear-gradient(135deg, rgba(64, 64, 64, 1) 0%, rgba(64, 64, 64, 0) 20%, rgba(64, 64, 64, 0.5) 100%);
|
||||
mask: linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<i18n>
|
||||
|
||||
Reference in New Issue
Block a user