Remove features de-planned for beta
This commit is contained in:
@@ -1,15 +1,7 @@
|
||||
import { useAuthStore } from '@/stores/authStore.js';
|
||||
import CTA01 from '@/views/CTA01.vue';
|
||||
import PaymentFailed from '@/views/PaymentFailed.vue';
|
||||
import CreatorList from '@/views/browser/CreatorList.vue';
|
||||
import ContentEditorPage from '@/views/contents/ContentEditorPage.vue';
|
||||
import ContentPage from '@/views/contents/ContentPage.vue';
|
||||
import PostContent from '@/views/contents/PostContent.vue';
|
||||
import CreatorContent from '@/views/creators/CreatorContent.vue';
|
||||
import CreatorHome from '@/views/creators/CreatorHome.vue';
|
||||
import CreatorLayout from '@/views/creators/CreatorLayout.vue';
|
||||
import ExclusiveContentCard from '@/views/creators/ExclusiveContentCard.vue';
|
||||
import SubscriptionMenu from '@/views/creators/SubscriptionMenu.vue';
|
||||
import About from '@/views/documentation/About.vue';
|
||||
import ContentPolicy from '@/views/documentation/ContentPolicy.vue';
|
||||
import CreatorGuide from '@/views/documentation/CreatorGuide.vue';
|
||||
@@ -23,37 +15,19 @@ import ProfilePage from '@/views/profile/ProfilePage.vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import LoginView from '../views/LoginView.vue';
|
||||
import PaymentCompleted from '../views/PaymentCompleted.vue';
|
||||
import Home from '../views/main/Home.vue';
|
||||
import Wallet from '../views/main/Wallet.vue';
|
||||
import Landing from '../views/main/Landing.vue';
|
||||
import CreateCreator from "@/views/creators/CreateCreator.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/cta01',
|
||||
component: CTA01,
|
||||
},
|
||||
{
|
||||
path: '/landing',
|
||||
name: 'landing',
|
||||
component: Home,
|
||||
component: Landing,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: { name: 'landing' },
|
||||
},
|
||||
{
|
||||
path: '/browse',
|
||||
component: CreatorList,
|
||||
redirect: { name: 'landing' }, // TODO remove this line when the page is ready
|
||||
},
|
||||
{
|
||||
path: '/content/editor',
|
||||
component: ContentEditorPage,
|
||||
},
|
||||
{
|
||||
path: '/content/:contentId',
|
||||
component: ContentPage,
|
||||
},
|
||||
{
|
||||
path: '/@:creator',
|
||||
component: CreatorLayout,
|
||||
@@ -62,20 +36,7 @@ const routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CreatorHome,
|
||||
},
|
||||
{
|
||||
path: 'content',
|
||||
component: CreatorContent,
|
||||
},
|
||||
{
|
||||
path: 'subscription',
|
||||
component: SubscriptionMenu,
|
||||
redirect: { name: 'creator' }, // TODO remove this line when the page is ready
|
||||
},
|
||||
{
|
||||
path: 'exclusivecontentcard',
|
||||
component: ExclusiveContentCard,
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -124,11 +85,6 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/content/post',
|
||||
component: PostContent,
|
||||
redirect: { name: 'landing' }, // TODO remove this line when the page is ready
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
@@ -145,12 +101,6 @@ const routes = [
|
||||
name: 'PaymentFailed',
|
||||
component: PaymentFailed,
|
||||
},
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'wallet',
|
||||
component: Wallet,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
<template>
|
||||
|
||||
<!-- lg et xl-->
|
||||
<div class="hidden lg:block xl:block background-container-lg">
|
||||
<img src="/images/hutopymedia/banners/hutopy.png" alt="Image bgwhite" class="block mx-auto max-w-[500px] mt-15"/>
|
||||
<div class="flex flex-row space-x-3.5 justify-center py-15">
|
||||
<div class="flex flex-column max-w-[500px]">
|
||||
<img src="/images/hutopymedia/others/ctaappdemo.png" alt="Image bgwhite" class="max-w-[500px]"/>
|
||||
</div>
|
||||
<div class="flex flex-column space-y-16 max-w-[475px] ma-12 text-justify">
|
||||
<h1 class="font-bold text-4xl font-serif">Monétisez votre contenu à sa vraie valeur.</h1>
|
||||
|
||||
<p>Nous créons une plateforme qui vous rémunère équitablement et vous aide à évoluer comme créateur. Rejoignez-nous et participez au prototype !</p>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<!-- Boîte pour courriel et bouton Participez -->
|
||||
<div class="flex items-center space-x-2 w-full">
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
label="Votre courriel"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
placeholder="Votre courriel"
|
||||
class="w-full mt-6"
|
||||
/>
|
||||
<v-btn class="text-white " height="60px" style="border-radius: 8px; background-color: #9F2E8D;">
|
||||
Participez
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" border-t-[4px] border-b-[4px] p-8 flex justify-center text-3xl text-white" style="background-color: #9F2E8D; border-color: #23393B;">
|
||||
|
||||
<div class="flex flex-row space-x-[250px]">
|
||||
<a href="https://www.facebook.com/Hutopy" target="_blank" aria-label="Facebook" class="hover:scale-125 transition-transform duration-300">
|
||||
<v-icon>mdi-facebook</v-icon>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/hutopy.inc/" target="_blank" aria-label="Instagram" class="hover:scale-125 transition-transform duration-300">
|
||||
<v-icon>mdi-instagram</v-icon>
|
||||
</a>
|
||||
<a href="https://x.com/Hutopyinc" target="_blank" aria-label="X" class="hover:scale-125 transition-transform duration-300">
|
||||
<v-icon>mdi-twitter</v-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- md et plus petit-->
|
||||
<div class="background-container-md block lg:hidden xl:hidden">
|
||||
<img src="/images/hutopymedia/banners/hutopy.png" alt="Image bgwhite" class="block mx-auto max-w-[400px] py-10"/>
|
||||
|
||||
<div class="max-w-[400px] mx-auto py-5">
|
||||
<img src="/images/hutopymedia/others/ctaappdemo.png" alt="Image bgwhite"/>
|
||||
</div>
|
||||
<div class="text-justify px-10">
|
||||
<h1 class="font-bold text-3xl font-serif hyphenated-text mb-10">Monétisez votre contenu à sa vraie valeur.</h1>
|
||||
|
||||
|
||||
|
||||
<v-text-field
|
||||
v-model="email"
|
||||
label="Votre courriel"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
placeholder="Votre courriel"
|
||||
class="w-full"
|
||||
density="compact"
|
||||
/>
|
||||
<v-btn class="text-white w-100" height="100px" style=" border-radius: 8px; background-color: #9F2E8D; font-size: 24px;">
|
||||
Participez
|
||||
</v-btn>
|
||||
|
||||
|
||||
|
||||
<p class="py-15">Nous créons une plateforme qui vous rémunère équitablement et vous aide à évoluer comme créateur. Rejoignez-nous et participez au prototype !</p>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class=" border-t-[4px] p-8 flex justify-center text-3xl text-white" style="background-color: #9F2E8D; border-color: #23393B;">
|
||||
|
||||
<div class="flex flex-row space-x-[100px]">
|
||||
<a href="https://www.facebook.com/Hutopy" target="_blank" aria-label="Facebook" class="hover:scale-125 transition-transform duration-300">
|
||||
<v-icon>mdi-facebook</v-icon>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/hutopy.inc/" target="_blank" aria-label="Instagram" class="hover:scale-125 transition-transform duration-300">
|
||||
<v-icon>mdi-instagram</v-icon>
|
||||
</a>
|
||||
<a href="https://x.com/Hutopyinc" target="_blank" aria-label="X" class="hover:scale-125 transition-transform duration-300">
|
||||
<v-icon>mdi-twitter</v-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.background-container-lg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-image: url('/images/hutopymedia/others/ctabgwhite.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.background-container-md {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
max-height: 1600px;
|
||||
background-image: url('/images/hutopymedia/others/ctabgwhite.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.hyphenated-text {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<v-card class="shadow-lg rounded-lg overflow-hidden max-w-sm">
|
||||
<v-img :src="creator.imageUrl" class="w-full h-48 object-cover"></v-img>
|
||||
<v-card-title class="text-lg font-bold">{{ creator.name }}</v-card-title>
|
||||
<v-card-subtitle class="text-sm text-gray-500">{{ creator.title }}</v-card-subtitle>
|
||||
<v-card-text class="text-base text-gray-700">{{ creator.description }}</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
defineProps({
|
||||
creator: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (profile) => {
|
||||
return 'image' in profile && 'name' in profile && 'title' in profile && 'description' in profile;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
|
||||
<v-img max-height="375"
|
||||
src="images/usersmedia/HutopyProfile/banners/banner01.png"
|
||||
cover>
|
||||
</v-img>
|
||||
|
||||
<div class="text-5xl font-semibold text-center py-10">
|
||||
CRÉATEURS
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2">
|
||||
<RouterLink v-for="(creator, index) in creators"
|
||||
:key="index"
|
||||
:to="creator.routerLink">
|
||||
<creator-card :creator="creator"
|
||||
class="m-2">
|
||||
</creator-card>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import CreatorCard from "@/views/browser/CreatorCard.vue";
|
||||
|
||||
</script>
|
||||
@@ -1,250 +0,0 @@
|
||||
<template>
|
||||
<div class="shadow-md rounded-2xl bg-gray-50 border custom-border">
|
||||
<div>
|
||||
<v-card-title>
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="props.content.createdByPortraitUrl"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="32px"
|
||||
height="32px">
|
||||
<div class="capitalize px-2">
|
||||
{{ props.content.createdByName }}
|
||||
</div>
|
||||
<span class="text-subtitle-2 mt-1">
|
||||
{{ time_ago(props.content.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" v-bind="props">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-if="creatorIsCurrentUser" @click="editContent">
|
||||
<v-list-item-title>Modifier le contenu</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="creatorIsCurrentUser" @click="openDeleteConfirmationDialog">
|
||||
<v-list-item-title>Effacer le contenu</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
<div class="uppercase">
|
||||
{{ props.content.title }}
|
||||
</div>
|
||||
<div>
|
||||
{{ props.content.description }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-carousel
|
||||
hide-delimiters
|
||||
v-if="hasUrls"
|
||||
:show-arrows="props.content.urls.length > 1"
|
||||
:show-indicators="props.content.urls.length > 1"
|
||||
>
|
||||
<v-carousel-item
|
||||
v-for="url in props.content.urls"
|
||||
:key="url"
|
||||
class="image-container"
|
||||
@click="redirectToContent"
|
||||
>
|
||||
<component :is="getComponent(url)" :src="url"></component>
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
</div>
|
||||
|
||||
<div class="px-4">
|
||||
<div class="flex justify-around py-2">
|
||||
<Reaction :content="content"></Reaction>
|
||||
|
||||
<v-btn
|
||||
:class="{'comment-active': hasMessages}"
|
||||
icon="true"
|
||||
variant="plain"
|
||||
@click="toggleComments">
|
||||
<v-icon>mdi-comment-outline</v-icon>
|
||||
{{ messageCount }}
|
||||
</v-btn>
|
||||
|
||||
<donation-button></donation-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div :class="{'hidden': !messagesVisible}">
|
||||
<h2 class="font-sans font-semibold mt-2">Commentaires</h2>
|
||||
<message-list
|
||||
:subject-id="props.content.id"
|
||||
:messages="messages"
|
||||
></message-list>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<post-message :subject-id="props.content.id"
|
||||
@message-posted="addMessage"
|
||||
></post-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<v-dialog v-model="openDeleteConfirmationModal" max-width="500">
|
||||
<v-form>
|
||||
<v-card class="text-center rounded-xl"
|
||||
:style="{
|
||||
border: `2px solid `
|
||||
}">
|
||||
<div class="flex items-center justify-between py-4 text-2xl font-bold border-b mb-2">
|
||||
<div class="flex-1 text-center">
|
||||
{{$t('contentCard.deletecontenttitle')}}
|
||||
</div>
|
||||
|
||||
<v-btn icon @click="openDeleteConfirmationModal = false" class="ml-auto mr-2" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
|
||||
<div class=" mr-2">
|
||||
Êtes-vous sûr de vouloir supprimer le contenu ?
|
||||
</div>
|
||||
|
||||
<div class="py-2 space-x-3">
|
||||
<v-btn variant="flat"
|
||||
@click="deleteContent()" class=" mt-5">
|
||||
Oui
|
||||
</v-btn>
|
||||
<v-btn variant="outlined"
|
||||
@click="openDeleteConfirmationModal = false" class=" mt-5">
|
||||
Non
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onBeforeMount, ref} from 'vue';
|
||||
import {time_ago} from "@/internal_time_ago.js";
|
||||
import MessageList from "@/views/messages/MessageList.vue";
|
||||
import PostMessage from "@/views/messages/PostMessage.vue";
|
||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
||||
import YoutubePlayer from './YoutubePlayer.vue';
|
||||
import ImageViewer from './ImageViewer.vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import Reaction from "@/views/contents/Reaction.vue";
|
||||
import {useMessageStore} from "@/stores/messageStore.js";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const openDeleteConfirmationModal = ref(false);
|
||||
const emits = defineEmits(['content-deleted'])
|
||||
|
||||
const contentId = computed(() => props.content.id)
|
||||
const creatorId = computed(() => props.content.createdBy)
|
||||
const creatorName = computed(() => props.content.createdByName)
|
||||
const creatorLogo = computed(() => props.content.createdByPortraitUrl)
|
||||
const colorMenu = computed(() => props.content.colorMenu)
|
||||
const colorAccent = computed(() => props.content.colorAccent)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const creatorIsCurrentUser = computed(() => authStore.isAuthenticated && authStore.userId === creatorId.value)
|
||||
const messageStore = useMessageStore();
|
||||
const messageCount = ref(0);
|
||||
|
||||
const hasUrls = computed(() => !!props.content.urls && props.content.urls.length > 0);
|
||||
const messagesVisible = ref(false);
|
||||
const messages = ref([]);
|
||||
const hasMessages = computed(() => messages.value.length > 0)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
messageCount.value = await messageStore.fetchMessageCount(contentId.value)
|
||||
})
|
||||
|
||||
function openDeleteConfirmationDialog() {
|
||||
openDeleteConfirmationModal.value = true;
|
||||
}
|
||||
|
||||
function addMessage(newMessage) {
|
||||
messages.value.unshift(newMessage);
|
||||
messagesVisible.value = true;
|
||||
messageCount.value ++;
|
||||
}
|
||||
|
||||
function toggleComments() {
|
||||
messagesVisible.value = !messagesVisible.value;
|
||||
}
|
||||
|
||||
function likeContent() {
|
||||
console.log('Content liked');
|
||||
}
|
||||
|
||||
function dislikeContent() {
|
||||
console.log('Content disliked');
|
||||
}
|
||||
|
||||
function getComponent(url) {
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
return YoutubePlayer;
|
||||
} else if (url.match(/\.(jpeg|jpg|gif|png)$/)) {
|
||||
return ImageViewer;
|
||||
}
|
||||
}
|
||||
|
||||
function editContent() {
|
||||
console.log('Modifier le contenu');
|
||||
}
|
||||
|
||||
async function deleteContent() {
|
||||
const client = useClient()
|
||||
const response = await client.delete(`/api/contents/${contentId.value}`)
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
emits('content-deleted', contentId.value)
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToContent() {
|
||||
window.location.href = `/content/${props.content.id}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border-color: #EAEBEC;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-active .v-icon {
|
||||
color: #D63DAB;
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import HTMLContentEditor from "@/views/contents/HTMLContentEditor.vue";
|
||||
import QuickyContentEditor from "@/views/contents/QuickyContentEditor.vue";
|
||||
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
|
||||
const showQuickyEditor = ref(true);
|
||||
const showHtmlEditor = ref(false);
|
||||
|
||||
const toggleQuickyEditor = () => {
|
||||
showQuickyEditor.value = true;
|
||||
showHtmlEditor.value = false;
|
||||
};
|
||||
|
||||
const toggleHtmlEditor = () => {
|
||||
showHtmlEditor.value = true;
|
||||
showQuickyEditor.value = false;
|
||||
};
|
||||
|
||||
const client = useClient();
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const creatorData = ref(null);
|
||||
const isLoading = ref(true); // Indicateur de chargement
|
||||
|
||||
const fetchCreatorData = async () => {
|
||||
const creatorName = creatorProfileStore.creator?.name;
|
||||
|
||||
if (!creatorName) {
|
||||
console.error("Nom du créateur introuvable dans le store.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.get(`/api/creators/@${creatorName}`);
|
||||
creatorData.value = response.data;
|
||||
console.log("Données du créateur récupérées :", creatorData.value);
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la récupération des données du créateur : ${error.response?.data || error.message}`);
|
||||
} finally {
|
||||
isLoading.value = false; // Indique que le chargement est terminé
|
||||
}
|
||||
};
|
||||
|
||||
// Appeler la fonction lors du montage du composant
|
||||
onMounted(() => {
|
||||
fetchCreatorData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isLoading" class="flex items-center justify-center h-screen">
|
||||
Chargement en cours...
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-else class="flex flex-col h-screen bg-hSurface text-hOnSurface">
|
||||
<div class="max-w-[1000px] mx-auto shadow-2xl rounded-lg overflow-hidden">
|
||||
<header class="text-2xl text-center py-4 bg-hPrimary text-hOnPrimary">
|
||||
Éditeur de contenu
|
||||
</header>
|
||||
|
||||
<div class="flex flex-grow">
|
||||
<aside class="side-menu flex flex-col items-center py-6 bg-hSecondary text-hOnSecondary">
|
||||
<div class="text-xl uppercase mb-6 px-2">Type de contenu</div>
|
||||
|
||||
<v-btn
|
||||
:variant="showQuickyEditor ? 'elevated' : 'plain'"
|
||||
@click="toggleQuickyEditor"
|
||||
class="mb-4 normal-button"
|
||||
>
|
||||
Quicky
|
||||
</v-btn>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
|
||||
<div v-if="showQuickyEditor">
|
||||
<QuickyContentEditor :creator-data="creatorData" />
|
||||
</div>
|
||||
|
||||
<div v-if="showHtmlEditor">
|
||||
<HTMLContentEditor />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.side-menu {
|
||||
background-color: #f7f7f7;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.normal-button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-infinite-scroll :items="contents" :onLoad="fetchContents">
|
||||
<div class="grid gap-2 -mt-4"
|
||||
:class="{
|
||||
'grid-cols-1': isExtraSmallScreen,
|
||||
'grid-cols-2': isSmallScreen,
|
||||
'grid-cols-3': isMediumScreen,
|
||||
'grid-cols-4': isLargeScreen,
|
||||
'grid-cols-5': isExtraLargeScreen
|
||||
}">
|
||||
<template v-for="content in contents" :key="content.id">
|
||||
<component
|
||||
:is="isSmallScreen ? ContentCardSm : ContentCardNormal"
|
||||
:content="content"
|
||||
@content-deleted="onContentDeleted"
|
||||
></component>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<template v-slot:empty >
|
||||
<div class="py-2 text-hOnSurface">Il n'y a pas plus de contenu</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:error>
|
||||
<v-alert type="error">{{ errorMessage }}</v-alert>
|
||||
</template>
|
||||
</v-infinite-scroll>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import ContentCardNormal from "@/views/contents/contentcards/NContentCard.vue";
|
||||
import ContentCardSm from "@/views/contents/contentcards/SmContentCard.vue";
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
||||
|
||||
const branding = useBrandingStore();
|
||||
const props = defineProps({
|
||||
creatorId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const client = useClient();
|
||||
const contents = ref([]);
|
||||
const errorMessage = ref();
|
||||
let last_id = null;
|
||||
|
||||
|
||||
const isExtraSmallScreen = ref(false);
|
||||
const isSmallScreen = ref(false);
|
||||
const isMediumScreen = ref(false);
|
||||
const isLargeScreen = ref(false);
|
||||
const isExtraLargeScreen = ref(false);
|
||||
|
||||
const updateScreenSize = () => {
|
||||
isExtraSmallScreen.value = window.matchMedia('(max-width: 640px)').matches;
|
||||
isSmallScreen.value = window.matchMedia('(min-width: 641px) and (max-width: 768px)').matches;
|
||||
isMediumScreen.value = window.matchMedia('(min-width: 769px) and (max-width: 1024px)').matches;
|
||||
isLargeScreen.value = window.matchMedia('(min-width: 1025px) and (max-width: 1280px)').matches;
|
||||
isExtraLargeScreen.value = window.matchMedia('(min-width: 1281px)').matches;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateScreenSize();
|
||||
window.addEventListener('resize', updateScreenSize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateScreenSize);
|
||||
});
|
||||
|
||||
|
||||
async function onContentDeleted(contentId) {
|
||||
contents.value = contents.value.filter(c => c.id !== contentId);
|
||||
}
|
||||
|
||||
const creatorIdWatcher = watch(
|
||||
() => props.creatorId,
|
||||
(newCreatorId) => {
|
||||
if (newCreatorId) {
|
||||
contents.value = [];
|
||||
last_id = null;
|
||||
fetchContents({ done: () => {} });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function fetchContents({ done, page_size = 10 }) {
|
||||
if (props.creatorId == null) return;
|
||||
|
||||
try {
|
||||
let uri = `/api/contents/creator/${props.creatorId}?page_size=${page_size}`;
|
||||
if (last_id !== null) uri = uri + `&last_id=${last_id}`;
|
||||
|
||||
const response = await client.get(uri);
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const contentCount = response.data.length;
|
||||
|
||||
if (contentCount > 0) {
|
||||
contents.value.push(...response.data);
|
||||
const [last_content] = response.data.slice(-1);
|
||||
last_id = last_content.id;
|
||||
}
|
||||
|
||||
if (contentCount < page_size)
|
||||
done('empty');
|
||||
else
|
||||
done('ok');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch posts", error);
|
||||
errorMessage.value = error.message || "Failed to fetch contents";
|
||||
done('error');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="d-sm-block d-md-block d-lg-none mb-1">
|
||||
<full-screen-content-sm></full-screen-content-sm>
|
||||
</div>
|
||||
<div class="d-none d-lg-flex">
|
||||
<full-screen-content-md></full-screen-content-md>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FullScreenContentSm from "@/views/contents/contentfullscreen/FullScreenContentSm.vue";
|
||||
import FullScreenContentMd from "@/views/contents/contentfullscreen/FullScreenContentMd.vue";
|
||||
</script>
|
||||
@@ -1,187 +0,0 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import '@tinymce/tinymce-vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
import {v7} from "uuid";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const client = useClient();
|
||||
|
||||
const tinymceScriptSrc = '/tinymce/js/tinymce/tinymce.min.js';
|
||||
const content = ref('');
|
||||
const title = ref('');
|
||||
let lastUploadedFileName = '';
|
||||
const isSnackbarOpen = ref(false);
|
||||
const snackbarTimeout = ref(2000);
|
||||
const snackbarText = ref('');
|
||||
const snackbarColor = ref('red');
|
||||
const selectedBackgroundColor = ref('#f0f0f0');
|
||||
|
||||
const userStore = useUserProfileStore();
|
||||
|
||||
// Custom image upload handler
|
||||
const imagesUploadHandler = async (blobInfo) => {
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('files', blobInfo.blob(), lastUploadedFileName);
|
||||
formData.append('creatorId', userStore.user.id);
|
||||
|
||||
let response = await client.post("/api/content/insert-image", formData);
|
||||
let imageUrl = response.data[0];
|
||||
|
||||
/* global tinymce */
|
||||
const editor = tinymce.activeEditor;
|
||||
const images = editor.dom.select('img');
|
||||
const lastImage = images.find(x => x.alt = lastUploadedFileName);
|
||||
|
||||
if (lastImage) {
|
||||
// Replace the source of the image
|
||||
editor.dom.setAttrib(lastImage, 'src', imageUrl);
|
||||
editor.dom.setAttrib(lastImage, 'alt', lastUploadedFileName);
|
||||
|
||||
// Adds the change to the undo stack
|
||||
editor.undoManager.add();
|
||||
} else {
|
||||
console.error('No image found in the content.');
|
||||
}
|
||||
};
|
||||
|
||||
const filePickerCallback = (callback, _) => {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
|
||||
input.onchange = function () {
|
||||
const file = input.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function () {
|
||||
lastUploadedFileName = file.name;
|
||||
callback(reader.result);
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
};
|
||||
|
||||
const saveAsync = async () => {
|
||||
if (title.value === '') {
|
||||
snackbarText.value = "Vous avez besoin d'un titre";
|
||||
isSnackbarOpen.value = true;
|
||||
}
|
||||
|
||||
const fullHtmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title.value}</title>
|
||||
</head>
|
||||
<body>
|
||||
${content.value}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('creatorId', userStore.user.id);
|
||||
formData.append('title', title.value);
|
||||
formData.append('htmlContent', fullHtmlContent);
|
||||
const response = await client.post("/api/contents/html", formData);
|
||||
|
||||
|
||||
if (response.status === 200) {
|
||||
snackbarText.value = "Publier";
|
||||
snackbarColor.value = "green";
|
||||
isSnackbarOpen.value = true;
|
||||
router.go(-1)
|
||||
}
|
||||
};
|
||||
|
||||
const setupTinyMCE = (editor) => {
|
||||
// Custom button for selecting background color
|
||||
editor.ui.registry.addButton('myCustomBgColorButton', {
|
||||
text: 'Page BG Color',
|
||||
onAction: function () {
|
||||
editor.windowManager.open({
|
||||
title: 'Select Page Background Color',
|
||||
body: {
|
||||
type: 'panel',
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
type: 'submit',
|
||||
primary: true
|
||||
}
|
||||
],
|
||||
onSubmit: function (dialog) {
|
||||
console.log('supppp');
|
||||
const color = dialog.getData().colorpicker;
|
||||
console.log(color);
|
||||
selectedBackgroundColor.value = color;
|
||||
|
||||
// Insert style into TinyMCE's content
|
||||
const styleTag = `<style>body { background-color: ${color}; }</style>`;
|
||||
editor.execCommand('mceInsertContent', false, styleTag);
|
||||
|
||||
dialog.close(); // Close dialog after selecting color
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-snackbar v-model="isSnackbarOpen" :timeout="snackbarTimeout">
|
||||
{{ snackbarText }}
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn :color="snackbarColor" variant="text" @click="isSnackbarOpen = false">
|
||||
Fermer
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
Html
|
||||
<v-btn class="mb-4 text-xl px-6 py-3" @click="router.go(-1)">Return</v-btn>
|
||||
<v-text-field
|
||||
v-model="title"
|
||||
placeholder="Title"
|
||||
style="width: 100%; font-size: 1.5rem; padding: 10px;"
|
||||
></v-text-field>
|
||||
<Editor
|
||||
style="max-width: 500px; width: 50%; font-size: 1.5rem; padding: 10px; height: 120%"
|
||||
:tinymceScriptSrc="tinymceScriptSrc"
|
||||
v-model="content"
|
||||
:init="{
|
||||
branding: false,
|
||||
promotion: false,
|
||||
plugins: 'lists link emoticons image imagetools code help wordcount media autoresize textcolor colorpicker',
|
||||
block_formats: 'Paragraph=p; Header 1=h1; Header 2=h2; Header 3=h3',
|
||||
toolbar: 'undo redo image align myCustomBgColorButton',
|
||||
automatic_uploads: true,
|
||||
file_picker_types: 'image',
|
||||
min_height: 600,
|
||||
max_height: 1200,
|
||||
images_upload_handler: imagesUploadHandler,
|
||||
file_picker_callback: filePickerCallback,
|
||||
// setup: setupTinyMCE, Possible to change background color of the html
|
||||
}"
|
||||
/>
|
||||
<v-btn @click="saveAsync()">POST</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<img :src="src" alt="Image" class="full-size-image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div class="flex flex-column">
|
||||
|
||||
<div class="h-full bg-yellow p-2 rounded-2xl m-4">
|
||||
<post-content-menu></post-content-menu>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column m-4 gap-4">
|
||||
<v-form>
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
label="Choisisez votre contenu"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
@change="onFileSelected"
|
||||
></v-file-input>
|
||||
|
||||
<v-img
|
||||
v-if="url"
|
||||
:src="url"
|
||||
max-height="375"
|
||||
contain
|
||||
></v-img>
|
||||
|
||||
<v-text-field
|
||||
v-model="title"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
label="Titre"
|
||||
hide-details
|
||||
clearable>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="description"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
label="Description"
|
||||
hide-details
|
||||
clearable>
|
||||
</v-text-field>
|
||||
|
||||
</v-form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 p-2 justify-end">
|
||||
<v-btn style="border-radius: 20px" variant="text">Canceller</v-btn>
|
||||
<v-btn style="border-radius: 20px" @click="publish">Publier</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// import posts from "@/views/posts/posts.json";
|
||||
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {ref} from 'vue';
|
||||
import PostContentMenu from "@/views/contents/PostContentMenu.vue";
|
||||
|
||||
const props = defineProps({
|
||||
contentId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const client = useClient()
|
||||
|
||||
const selectedFile = ref("")
|
||||
const url = ref("")
|
||||
const title = ref("")
|
||||
const description = ref("")
|
||||
|
||||
const onFileSelected = () => {
|
||||
if (selectedFile.value) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.readAsDataURL(selectedFile.value);
|
||||
fileReader.onload = () => {
|
||||
url.value = fileReader.result;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const publish = async () => {
|
||||
const response = await client.post(
|
||||
`/api/contents/`,
|
||||
{
|
||||
"url": url.value,
|
||||
"title": title.value,
|
||||
"description": description.value
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.info(`Content created!`)
|
||||
} else {
|
||||
console.error(`Failed to create content ${response.data}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div class="flow">
|
||||
<button @click="selectType('title')">
|
||||
<v-icon>mdi-format-title</v-icon>
|
||||
</button>
|
||||
|
||||
<button @click="selectType('text')">
|
||||
<v-icon>mdi-text</v-icon>
|
||||
</button>
|
||||
|
||||
<button @click="selectType('image')">
|
||||
<v-icon>mdi-image</v-icon>
|
||||
</button>
|
||||
<button @click="selectType('video')">
|
||||
<v-icon>mdi-video</v-icon>
|
||||
</button>
|
||||
|
||||
<button @click="selectType('audio')">
|
||||
<v-icon>mdi-volume-high</v-icon>
|
||||
</button>
|
||||
|
||||
<button @click="selectType('comments')">
|
||||
<v-icon>mdi-comment</v-icon>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Affichage du contenu en fonction du type sélectionné -->
|
||||
<!-- <v-card-text>-->
|
||||
<!-- <v-row v-for="(content, index) in contents" :key="index" class="draggable-row"-->
|
||||
<!-- @dragstart="dragStart(index)" @dragover.prevent @drop="drop(index)" draggable="true">-->
|
||||
<!-- <v-col cols="10">-->
|
||||
<!-- <template v-if="content.type === 'title'">-->
|
||||
<!-- <v-text-field v-model="content.value" label="Titre"></v-text-field>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else-if="content.type === 'text'">-->
|
||||
<!-- <v-textarea v-model="content.value" label="Texte"></v-textarea>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else-if="content.type === 'image'">-->
|
||||
<!-- <v-row>-->
|
||||
|
||||
<!-- <v-col cols="12">-->
|
||||
<!-- <v-file-input v-model="content.value" label="Image"></v-file-input>-->
|
||||
<!-- </v-col>-->
|
||||
<!-- </v-row>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else-if="content.type === 'video'">-->
|
||||
<!-- <v-text-field v-model="content.value" label="URL de la vidéo"></v-text-field>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else-if="content.type === 'audio'">-->
|
||||
<!-- <v-row>-->
|
||||
<!-- <v-col cols="2">-->
|
||||
<!-- <v-icon>mdi-volume-high</v-icon>-->
|
||||
<!-- </v-col>-->
|
||||
<!-- <v-col cols="10">-->
|
||||
<!-- <v-file-input v-model="content.value" label="Audio"></v-file-input>-->
|
||||
<!-- </v-col>-->
|
||||
<!-- </v-row>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else-if="content.type === 'comments'">-->
|
||||
<!-- <v-text-field v-model="content.value" label="Commentaires"></v-text-field>-->
|
||||
<!-- </template>-->
|
||||
<!-- </v-col>-->
|
||||
<!-- <v-col cols="2" class="d-flex justify-center align-center">-->
|
||||
<!-- <button icon @click="removeContent(index)" class="remove-button">-->
|
||||
<!-- <v-icon>mdi-close</v-icon>-->
|
||||
<!-- </button>-->
|
||||
<!-- </v-col>-->
|
||||
<!-- </v-row>-->
|
||||
<!-- </v-card-text>-->
|
||||
|
||||
<!-- <!– Boutons Post, Preview et Cancel –>-->
|
||||
<!-- <v-row v-if="contents.length > 0" justify="end" style="margin-bottom: 10px;">-->
|
||||
<!-- <v-col class="d-flex justify-end" style="margin-right: 4%;">-->
|
||||
<!-- <button style="margin-right: 15px;" @click="postContent" color="white" dark-->
|
||||
<!-- elevation="4">Post-->
|
||||
<!-- </button>-->
|
||||
<!-- <button style="margin-right: 15px;" @click="previewContent" color="white" dark-->
|
||||
<!-- elevation="5">Preview-->
|
||||
<!-- </button>-->
|
||||
<!-- <button @click="cancelPost" color="white" dark elevation="5">Cancel</button>-->
|
||||
<!-- </v-col>-->
|
||||
<!-- </v-row>-->
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const contents = ref([]);
|
||||
let dragIndex = null;
|
||||
|
||||
const selectType = (type) => {
|
||||
console.log("Type sélectionné:", type);
|
||||
contents.value.push({type: type, value: ''});
|
||||
};
|
||||
|
||||
const removeContent = (index) => {
|
||||
contents.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const postContent = () => {
|
||||
// Implémenter la logique pour poster le contenu
|
||||
};
|
||||
|
||||
const previewContent = () => {
|
||||
// Implémenter la logique pour prévisualiser le contenu
|
||||
};
|
||||
|
||||
const cancelPost = () => {
|
||||
if (contents.value.length > 0) {
|
||||
// Réinitialiser le tableau contents pour supprimer tous les contenus
|
||||
contents.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const dragStart = (index) => {
|
||||
dragIndex = index;
|
||||
};
|
||||
|
||||
const drop = (index) => {
|
||||
if (dragIndex !== null && index !== null) {
|
||||
const draggedItem = contents.value[dragIndex];
|
||||
contents.value.splice(dragIndex, 1);
|
||||
contents.value.splice(index, 0, draggedItem);
|
||||
dragIndex = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.remove-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -20%;
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
.draggable-row {
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
@@ -1,154 +0,0 @@
|
||||
<script setup>
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {ref} from 'vue';
|
||||
import {v7} from 'uuid';
|
||||
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
|
||||
const emits = defineEmits(['content-posted'])
|
||||
|
||||
const brandingStore = useBrandingStore()
|
||||
const creatorProfileStore = useCreatorProfileStore()
|
||||
|
||||
const isDialogActive = ref(false);
|
||||
|
||||
const client = useClient();
|
||||
const title = ref('');
|
||||
const message = ref('');
|
||||
const files = ref([]);
|
||||
const externalUrls = ref([]);
|
||||
|
||||
const addUrl = () => {
|
||||
externalUrls.value.push('');
|
||||
};
|
||||
|
||||
const removeUrl = (index) => {
|
||||
externalUrls.value.splice(index, 1);
|
||||
};
|
||||
|
||||
async function publishPost() {
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('creatorId', creatorProfileStore.creator.id);
|
||||
formData.append('title', title.value);
|
||||
formData.append('description', message.value);
|
||||
files.value.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
externalUrls.value.forEach(externalUrl => {
|
||||
formData.append('externalUrls', externalUrl);
|
||||
});
|
||||
|
||||
try {
|
||||
const content = await client.post(
|
||||
`/api/contents/`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
})
|
||||
|
||||
emits('content-posted', content.data)
|
||||
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const cancelPost = () => {
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
isDialogActive.value = false;
|
||||
title.value = '';
|
||||
message.value = '';
|
||||
files.value = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class=" items-center transform transition-transform duration-200 hover:text-gray-300 hover:scale-125 px-4"
|
||||
@click="isDialogActive = true">
|
||||
<v-icon style="font-size: 25px; height: 25px; width: 55px;">mdi-text-box-plus-outline</v-icon>
|
||||
</button>
|
||||
|
||||
<v-dialog v-model="isDialogActive" max-width="500">
|
||||
<v-form>
|
||||
<v-card class="text-center rounded-xl border-2 border-solid border-hSecondary">
|
||||
|
||||
<v-card-title class="font-medium">
|
||||
Créer un Contenu
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field v-model="title"
|
||||
class="p-2"
|
||||
label="Titre"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea v-model="message"
|
||||
label="Écrivez votre message ici..."
|
||||
class="p-2"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
outlined
|
||||
></v-textarea>
|
||||
|
||||
<div v-for="(url, index) in externalUrls" :key="index" class="d-flex align-center">
|
||||
<v-text-field
|
||||
v-model="externalUrls[index]"
|
||||
class="p-2 flex-grow-1"
|
||||
label="Url Externe"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<v-btn icon @click="removeUrl(index)" class="ml-2">
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn icon @click="addUrl" class="mt-2">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-file-input v-model="files"
|
||||
label="Glissez vos images"
|
||||
class="p-2 custom-file-input"
|
||||
variant="outlined"
|
||||
multiple
|
||||
dropzone
|
||||
prepend-icon=""
|
||||
placeholder="Glissez et déposez des fichiers ici ou cliquez pour sélectionner des fichiers"
|
||||
></v-file-input>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="flat"
|
||||
@click="cancelPost"
|
||||
class="p-20">
|
||||
Cancel
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="flat"
|
||||
color="primary"
|
||||
@click="publishPost">
|
||||
Publier
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import { v7 } from "uuid";
|
||||
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const client = useClient();
|
||||
const router = useRouter();
|
||||
const step = ref(1);
|
||||
const title = ref('');
|
||||
const message = ref('');
|
||||
const files = ref([]);
|
||||
const Thumbnail = ref();
|
||||
const ThumbnailPreview = ref(null);
|
||||
const externalUrls = ref([]);
|
||||
const warningMessage = ref('');
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const carouselIndex = ref(0);
|
||||
|
||||
|
||||
const carouselItems = computed(() => {
|
||||
const images = files.value.map(file => URL.createObjectURL(file));
|
||||
const videos = externalUrls.value.filter(url => url.trim() !== '');
|
||||
return [...images, ...videos];
|
||||
});
|
||||
|
||||
watch(Thumbnail, (newFile) => {
|
||||
if (newFile) {
|
||||
ThumbnailPreview.value = URL.createObjectURL(newFile);
|
||||
} else {
|
||||
ThumbnailPreview.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const addUrl = () => externalUrls.value.push('');
|
||||
const removeUrl = (index) => externalUrls.value.splice(index, 1);
|
||||
|
||||
|
||||
const nextCarouselItem = () => {
|
||||
if (carouselItems.value.length > 0) {
|
||||
carouselIndex.value = (carouselIndex.value + 1) % carouselItems.value.length;
|
||||
}
|
||||
};
|
||||
const previousCarouselItem = () => {
|
||||
if (carouselItems.value.length > 0) {
|
||||
carouselIndex.value =
|
||||
(carouselIndex.value - 1 + carouselItems.value.length) %
|
||||
carouselItems.value.length;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const goToContentEditor = () => {
|
||||
if (!title.value || !Thumbnail.value) {
|
||||
warningMessage.value = 'Veuillez sélectionner un thumbnail et entrer un titre avant de continuer.';
|
||||
return;
|
||||
}
|
||||
warningMessage.value = '';
|
||||
step.value = 2;
|
||||
};
|
||||
|
||||
|
||||
const resetForm = () => {
|
||||
title.value = '';
|
||||
message.value = '';
|
||||
files.value = [];
|
||||
Thumbnail.value = null;
|
||||
ThumbnailPreview.value = null;
|
||||
externalUrls.value = [];
|
||||
step.value = 1;
|
||||
};
|
||||
|
||||
const publishPost = async () => {
|
||||
if (!Thumbnail.value) {
|
||||
alert("Veuillez ajouter un thumbnail avant de publier.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('creatorId', creatorProfileStore.creator.id);
|
||||
formData.append('title', title.value);
|
||||
formData.append('description', message.value);
|
||||
formData.append('Thumbnail', Thumbnail.value);
|
||||
|
||||
files.value.forEach(file => formData.append('files', file));
|
||||
externalUrls.value.forEach(url => formData.append('externalUrls', url));
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/contents`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
console.log('Content published:', response.data);
|
||||
|
||||
|
||||
const creatorName = creatorProfileStore.creator.name;
|
||||
router.push(`/@${creatorName}/content`);
|
||||
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
console.error('Error publishing content:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-xl p-4 shadow-md rounded-lg bg-white overflow-y-auto w-[1000px]">
|
||||
<!-- Thumbnail Editor -->
|
||||
<div v-if="step === 1" class="flex flex-col items-center">
|
||||
<h2 class="text-lg font-bold mb-4">Éditeur de Thumbnail</h2>
|
||||
|
||||
<!-- Thumbnail Preview -->
|
||||
<div
|
||||
class="shadow-md rounded-md bg-gray-50 border custom-border w-[400px] h-[250px] mb-4 flex items-center justify-center cursor-pointer"
|
||||
@click="$refs.thumbnailInput.click()"
|
||||
>
|
||||
<img
|
||||
v-if="ThumbnailPreview"
|
||||
:src="ThumbnailPreview"
|
||||
class="rounded-md w-full h-full object-cover"
|
||||
alt="Thumbnail Preview"
|
||||
/>
|
||||
<div v-else class="text-gray-500">Cliquez pour sélectionner une image</div>
|
||||
</div>
|
||||
|
||||
<!-- Titre -->
|
||||
<v-text-field
|
||||
v-model="title"
|
||||
label="Titre"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
clearable
|
||||
class="mb-4 w-[400px]"
|
||||
/>
|
||||
|
||||
<!-- Upload Thumbnail -->
|
||||
<v-file-input
|
||||
ref="thumbnailInput"
|
||||
v-model="Thumbnail"
|
||||
label="Télécharger un Thumbnail"
|
||||
variant="outlined"
|
||||
dropzone
|
||||
clearable
|
||||
class="mb-6 w-[400px]"
|
||||
/>
|
||||
|
||||
<!-- Message d'avertissement -->
|
||||
<p v-if="warningMessage" class="text-red-500 text-sm mb-4">{{ warningMessage }}</p>
|
||||
|
||||
<!-- Bouton pour passer à l'étape suivante -->
|
||||
<v-btn color="primary" variant="contained" class="w-[400px]" @click="goToContentEditor">
|
||||
Next
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Content Editor -->
|
||||
<div v-if="step === 2" class="flex flex-col items-center">
|
||||
<h2 class="text-lg font-bold mb-4">Content Editor</h2>
|
||||
|
||||
<!-- Carrousel -->
|
||||
<div class="relative w-[400px] h-[250px] mb-6">
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-black/50 rounded-md"
|
||||
v-if="carouselItems.length === 0"
|
||||
>
|
||||
<p class="text-white">Aucun élément à afficher</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Image ou vidéo en cours -->
|
||||
<img
|
||||
v-if="carouselItems[carouselIndex].includes('blob')"
|
||||
:src="carouselItems[carouselIndex]"
|
||||
class="carousel-item"
|
||||
alt="Carrousel Image"
|
||||
/>
|
||||
<iframe
|
||||
v-else
|
||||
:src="`${carouselItems[carouselIndex]}?autoplay=1&mute=1`"
|
||||
frameborder="0"
|
||||
allow="autoplay"
|
||||
class="carousel-item"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
<!-- Flèches pour naviguer -->
|
||||
<button
|
||||
class="absolute top-1/2 left-2 transform -translate-y-1/2 text-white bg-black/50 rounded-full p-2"
|
||||
@click="previousCarouselItem"
|
||||
>
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="absolute top-1/2 right-2 transform -translate-y-1/2 text-white bg-black/50 rounded-full p-2"
|
||||
@click="nextCarouselItem"
|
||||
>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<v-textarea
|
||||
v-model="message"
|
||||
label="Message"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
clearable
|
||||
class="mb-4 w-[400px]"
|
||||
/>
|
||||
|
||||
<!-- Ajout des URLs externes -->
|
||||
<div v-for="(url, index) in externalUrls" :key="index" class="flex space-x-2 w-[400px]">
|
||||
<v-text-field
|
||||
v-model="externalUrls[index]"
|
||||
label="Lien URL"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
class="flex-1"
|
||||
/>
|
||||
<v-btn icon color="error" @click="removeUrl(index)">
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="w-[400px] mb-10">
|
||||
<div class="flex items-center gap-2">
|
||||
<v-btn icon color="primary" @click="addUrl">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<span class="text-sm text-gray-500">Cliquez pour ajouter un nouveau lien vidéo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Upload Images -->
|
||||
<v-file-input
|
||||
v-model="files"
|
||||
label="Télécharger des Images"
|
||||
variant="outlined"
|
||||
multiple
|
||||
dropzone
|
||||
class="mb-6 w-[400px]"
|
||||
/>
|
||||
|
||||
<!-- Boutons de navigation -->
|
||||
<div class="flex w-[400px] justify-between">
|
||||
<v-btn variant="outlined" @click="step = 1">
|
||||
Back
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="contained" @click="publishPost">
|
||||
Publier
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.bg-black\/50 {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #f56565;
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border-color: #eaebec;
|
||||
}
|
||||
|
||||
/* Taille fixe pour les éléments du carrousel */
|
||||
.carousel-item {
|
||||
width: 400px;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.375rem;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,332 +0,0 @@
|
||||
<script setup>
|
||||
import { REACTIONS } from "@/Constants/Reactions.js";
|
||||
import { computed, ref } from "vue";
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js"
|
||||
import MustBeLogged from "@/views/MustBeLogged.vue";
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
|
||||
const userProfileStore = useUserProfileStore();
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const contentId = computed(() => props.content.id);
|
||||
|
||||
const hasReacted = ref(false);
|
||||
const currentReaction = ref(null);
|
||||
const likeCount = ref(0);
|
||||
const dislikeCount = ref(0);
|
||||
const loveCount = ref(0);
|
||||
const hahaCount = ref(0);
|
||||
const wowCount = ref(0);
|
||||
const sadCount = ref(0);
|
||||
const angryCount = ref(0);
|
||||
|
||||
const menuVisible = ref(false);
|
||||
const holdTimeout = ref(null);
|
||||
const hideTimeout = ref(null);
|
||||
const touchTimeout = ref(null);
|
||||
|
||||
const loginModal = ref(false);
|
||||
|
||||
initializeReactions();
|
||||
|
||||
async function reactToContent(reaction) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
loginModal.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const client = useClient();
|
||||
|
||||
if (!hasReacted.value) {
|
||||
const request = {
|
||||
ContentId: contentId.value,
|
||||
reaction: reaction,
|
||||
userId: userProfileStore.user.id,
|
||||
userName: `${userProfileStore.user.firstName} ${userProfileStore.user.lastName}`,
|
||||
};
|
||||
adjustReactionCount(reaction);
|
||||
await client.post("/api/content/reaction/", request);
|
||||
|
||||
hasReacted.value = true;
|
||||
console.log(`Added ${reaction} reaction to content.`);
|
||||
} else if (reaction !== currentReaction.value) {
|
||||
const requestAdd = {
|
||||
ContentId: contentId.value,
|
||||
reaction: reaction,
|
||||
userId: userProfileStore.user.id,
|
||||
userName: `${userProfileStore.user.firstName} ${userProfileStore.user.lastName}`,
|
||||
};
|
||||
adjustReactionCount(reaction);
|
||||
await client.post("/api/content/reaction/", requestAdd);
|
||||
|
||||
console.log(`Changed reaction to ${reaction} on content.`);
|
||||
} else {
|
||||
const requestRemove = {
|
||||
ContentId: contentId.value,
|
||||
userId: userProfileStore.user.id,
|
||||
};
|
||||
adjustReactionCount(reaction);
|
||||
await client.post("/api/content/reaction/remove", requestRemove);
|
||||
|
||||
hasReacted.value = false;
|
||||
console.log("Reaction to content removed.");
|
||||
}
|
||||
setTimeout(() => {
|
||||
menuVisible.value = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function adjustReactionCount(newReaction) {
|
||||
if (currentReaction.value === newReaction) {
|
||||
switch (newReaction) {
|
||||
case REACTIONS.LIKE:
|
||||
if (likeCount.value > 0) likeCount.value--;
|
||||
break;
|
||||
case REACTIONS.DISLIKE:
|
||||
if (dislikeCount.value > 0) dislikeCount.value--;
|
||||
break;
|
||||
case REACTIONS.LOVE:
|
||||
if (loveCount.value > 0) loveCount.value--;
|
||||
break;
|
||||
case REACTIONS.HAHA:
|
||||
if (hahaCount.value > 0) hahaCount.value--;
|
||||
break;
|
||||
case REACTIONS.WOW:
|
||||
if (wowCount.value > 0) wowCount.value--;
|
||||
break;
|
||||
case REACTIONS.SAD:
|
||||
if (sadCount.value > 0) sadCount.value--;
|
||||
break;
|
||||
case REACTIONS.ANGRY:
|
||||
if (angryCount.value > 0) angryCount.value--;
|
||||
break;
|
||||
}
|
||||
currentReaction.value = null;
|
||||
hasReacted.value = false;
|
||||
} else {
|
||||
if (currentReaction.value) {
|
||||
switch (currentReaction.value) {
|
||||
case REACTIONS.LIKE:
|
||||
if (likeCount.value > 0) likeCount.value--;
|
||||
break;
|
||||
case REACTIONS.DISLIKE:
|
||||
if (dislikeCount.value > 0) dislikeCount.value--;
|
||||
break;
|
||||
case REACTIONS.LOVE:
|
||||
if (loveCount.value > 0) loveCount.value--;
|
||||
break;
|
||||
case REACTIONS.HAHA:
|
||||
if (hahaCount.value > 0) hahaCount.value--;
|
||||
break;
|
||||
case REACTIONS.WOW:
|
||||
if (wowCount.value > 0) wowCount.value--;
|
||||
break;
|
||||
case REACTIONS.SAD:
|
||||
if (sadCount.value > 0) sadCount.value--;
|
||||
break;
|
||||
case REACTIONS.ANGRY:
|
||||
if (angryCount.value > 0) angryCount.value--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (newReaction) {
|
||||
case REACTIONS.LIKE:
|
||||
likeCount.value++;
|
||||
break;
|
||||
case REACTIONS.DISLIKE:
|
||||
dislikeCount.value++;
|
||||
break;
|
||||
case REACTIONS.LOVE:
|
||||
loveCount.value++;
|
||||
break;
|
||||
case REACTIONS.HAHA:
|
||||
hahaCount.value++;
|
||||
break;
|
||||
case REACTIONS.WOW:
|
||||
wowCount.value++;
|
||||
break;
|
||||
case REACTIONS.SAD:
|
||||
sadCount.value++;
|
||||
break;
|
||||
case REACTIONS.ANGRY:
|
||||
angryCount.value++;
|
||||
break;
|
||||
}
|
||||
|
||||
currentReaction.value = newReaction;
|
||||
hasReacted.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeReactions() {
|
||||
const userReaction = props.content.reactions.find((x) => x.userId === userProfileStore.user.id);
|
||||
if (userReaction) {
|
||||
currentReaction.value = userReaction.reaction;
|
||||
hasReacted.value = true;
|
||||
} else {
|
||||
currentReaction.value = null;
|
||||
hasReacted.value = false;
|
||||
}
|
||||
|
||||
likeCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.LIKE).length;
|
||||
dislikeCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.DISLIKE).length;
|
||||
loveCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.LOVE).length;
|
||||
hahaCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.HAHA).length;
|
||||
wowCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.WOW).length;
|
||||
sadCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.SAD).length;
|
||||
angryCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.ANGRY).length;
|
||||
}
|
||||
|
||||
function showReactions() {
|
||||
clearTimeout(hideTimeout.value);
|
||||
menuVisible.value = true;
|
||||
}
|
||||
|
||||
function hideReactions() {
|
||||
hideTimeout.value = setTimeout(() => {
|
||||
menuVisible.value = false;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function onTouchStart() {
|
||||
touchTimeout.value = setTimeout(() => {
|
||||
menuVisible.value = true;
|
||||
}, 250);
|
||||
}
|
||||
function onTouchEnd() {
|
||||
clearTimeout(touchTimeout.value);
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
clearTimeout(holdTimeout.value);
|
||||
hideReactions();
|
||||
}
|
||||
|
||||
function onMouseOver() {
|
||||
if (!isMobileDevice()) {
|
||||
showReactions();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
if (!isMobileDevice()) {
|
||||
hideReactions();
|
||||
}
|
||||
}
|
||||
|
||||
function keepReactionMenuOpen(){
|
||||
clearTimeout(hideTimeout.value);
|
||||
}
|
||||
|
||||
function isMobileDevice() {
|
||||
return window.innerWidth <= 800;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<v-menu
|
||||
class="reaction-card"
|
||||
v-model="menuVisible"
|
||||
offset-y
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
:attach="$el"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
v-bind="attrs"
|
||||
:on="on"
|
||||
icon="true"
|
||||
variant="plain"
|
||||
@touchstart="onTouchStart"
|
||||
@touchend="onTouchEnd"
|
||||
@mouseup="onMouseUp"
|
||||
@mouseover="onMouseOver"
|
||||
@mouseleave="onMouseLeave"
|
||||
@click="reactToContent(REACTIONS.LIKE)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.LIKE}">mdi-thumb-up-outline</v-icon>
|
||||
{{ likeCount }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card
|
||||
class="reaction-card"
|
||||
@mouseover="keepReactionMenuOpen"
|
||||
@mouseleave="hideReactions"
|
||||
>
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="reactToContent(REACTIONS.DISLIKE)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.DISLIKE}">mdi-thumb-down-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="reactToContent(REACTIONS.LOVE)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.LOVE}">mdi-heart-outline</v-icon>
|
||||
{{ loveCount }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="reactToContent(REACTIONS.HAHA)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.HAHA}">mdi-emoticon-excited-outline</v-icon>
|
||||
{{ hahaCount }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="reactToContent(REACTIONS.WOW)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.WOW}">mdi-emoticon-happy-outline</v-icon>
|
||||
{{ wowCount }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="reactToContent(REACTIONS.SAD)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.SAD}">mdi-emoticon-sad-outline</v-icon>
|
||||
{{ sadCount }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="plain"
|
||||
@click="reactToContent(REACTIONS.ANGRY)"
|
||||
>
|
||||
<v-icon :class="{'active-icon': currentReaction === REACTIONS.ANGRY}">mdi-emoticon-angry-outline</v-icon>
|
||||
{{ angryCount }}
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<must-be-logged v-model="loginModal" message="Vous devez être connecté pour réagir."></must-be-logged>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reaction-card {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 8px;
|
||||
margin-top: -35px;
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.active-icon {
|
||||
color: blue;
|
||||
stroke: blue;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<iframe
|
||||
:src="src"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { time_ago } from "@/internal_time_ago.js";
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import { useAuthStore } from "@/stores/authStore.js";
|
||||
import { useMessageStore } from "@/stores/messageStore.js";
|
||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const openDeleteConfirmationModal = ref(false);
|
||||
const emits = defineEmits(['content-deleted']);
|
||||
const contentId = computed(() => props.content.id);
|
||||
const creatorId = computed(() => props.content.createdBy);
|
||||
const Thumbnail = computed(() => props.content.thumbnailUrl);
|
||||
const branding = useBrandingStore();
|
||||
const authStore = useAuthStore();
|
||||
const creatorIsCurrentUser = computed(() => authStore.isAuthenticated && authStore.userId === creatorId.value);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const messageStore = useMessageStore();
|
||||
messageStore.fetchMessageCount(contentId.value);
|
||||
});
|
||||
|
||||
function openDeleteConfirmationDialog() {
|
||||
openDeleteConfirmationModal.value = true;
|
||||
}
|
||||
|
||||
async function deleteContent() {
|
||||
const client = useClient();
|
||||
const response = await client.delete(`/api/contents/${contentId.value}`);
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
emits('content-deleted', contentId.value);
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToContent() {
|
||||
window.location.href = `/content/${props.content.id}`;
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const bigint = parseInt(hex.replace('#', ''), 16);
|
||||
return `${(bigint >> 16) & 255}, ${(bigint >> 8) & 255}, ${bigint & 255}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-border {
|
||||
border-color: #EAEBEC;
|
||||
}
|
||||
|
||||
.comment-active .v-icon {
|
||||
color: #D63DAB;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<div class="shadow-md rounded-md bg-gray-50 border custom-border w-52 h-[300px] bg-hSurface"
|
||||
:style="{
|
||||
boxShadow: '0 10px 10px rgba(0, 0, 0, 0.3)',
|
||||
borderColor: `rgba(${hexToRgb('--h-secondary')}, 0.4)`,
|
||||
borderWidth: '1px',
|
||||
}">
|
||||
|
||||
<img
|
||||
v-if="props.content.thumbnailUrl"
|
||||
:src="props.content.thumbnailUrl.replace(/[{}]/g, '')"
|
||||
class="rounded-t-md w-[260px] h-[160px] object-cover cursor-pointer"
|
||||
alt="Image Content"
|
||||
@click="redirectToContent" />
|
||||
|
||||
<div class="p-1">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<span class="text-caption mt-1 px-2 text-hOnSurface">
|
||||
{{ time_ago(props.content.createdAt) }}
|
||||
</span>
|
||||
<v-menu v-if="creatorIsCurrentUser" :offset-y="true">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain"
|
||||
v-bind="props"
|
||||
style="min-width: auto; padding: 0; margin-right: 4px;">
|
||||
<div class="text-hOnSurface">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</div>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="openDeleteConfirmationDialog">
|
||||
<v-list-item-title>{{$t('contentCard.delete')}}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="capitalize p-2 text-hOnSurface">{{ props.content.title }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Dialog -->
|
||||
<v-dialog v-model="openDeleteConfirmationModal" max-width="500">
|
||||
<v-card class="text-center rounded-xl">
|
||||
<div class="flex items-center justify-between py-4 text-2xl font-bold border-b mb-2">
|
||||
<div class="flex-1 text-center">{{$t('contentCard.deletecontenttitle')}}</div>
|
||||
<v-btn icon @click="openDeleteConfirmationModal = false" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div>{{$t('contentCard.deeletecontentwarning')}}</div>
|
||||
<div class="py-2 space-x-3">
|
||||
<v-btn variant="flat" @click="deleteContent()" class="mt-5">{{$t('general.yes')}}</v-btn>
|
||||
<v-btn variant="outlined" @click="openDeleteConfirmationModal = false" class="mt-5">{{$t('general.no')}}</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
<template>
|
||||
<div class="shadow-md bg-gray-50">
|
||||
<div>
|
||||
<v-card-title>
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="props.content.createdByPortraitUrl"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="32px"
|
||||
height="32px">
|
||||
<router-link class="capitalize px-2" :to="`/@${props.content.createdByName}`">
|
||||
{{ props.content.createdByName }}
|
||||
</router-link>
|
||||
<span class="text-subtitle-2">
|
||||
{{ time_ago(props.content.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" v-bind="props">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-if="creatorIsCurrentUser" @click="editContent">
|
||||
<v-list-item-title>{{ $t('contentCard.edit') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="creatorIsCurrentUser" @click="openDeleteConfirmationDialog">
|
||||
<v-list-item-title>{{ $t('contentCard.delete') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
<div class="uppercase">
|
||||
{{ props.content.title }}
|
||||
</div>
|
||||
<div>
|
||||
{{ props.content.description }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-carousel
|
||||
hide-delimiters
|
||||
v-if="hasUrls"
|
||||
:show-arrows="props.content.urls.length > 1"
|
||||
:show-indicators="props.content.urls.length > 1"
|
||||
>
|
||||
<v-carousel-item
|
||||
v-for="url in props.content.urls"
|
||||
:key="url"
|
||||
class="image-container"
|
||||
@click="redirectToContent"
|
||||
>
|
||||
<component :is="getComponent(url)" :src="url"></component>
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
</div>
|
||||
|
||||
<div class="px-1">
|
||||
<div class="flex justify-around ">
|
||||
<Reaction :content="content"></Reaction>
|
||||
|
||||
<v-btn
|
||||
:class="{'comment-active': hasMessages}"
|
||||
icon="true"
|
||||
variant="plain"
|
||||
@click="toggleComments">
|
||||
<v-icon>mdi-comment-outline</v-icon>
|
||||
{{ messageCount }}
|
||||
</v-btn>
|
||||
|
||||
<donation-button></donation-button>
|
||||
|
||||
</div>
|
||||
|
||||
<div :class="{'hidden': !messagesVisible}">
|
||||
<h2 class="font-sans font-semibold ">{{ $t('contentCard.commenttitle') }}</h2>
|
||||
<message-list
|
||||
:subject-id="props.content.id"
|
||||
:messages="messages"
|
||||
></message-list>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="py-1">
|
||||
<post-message :subject-id="props.content.id"
|
||||
@message-posted="addMessage"
|
||||
></post-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<v-dialog v-model="openDeleteConfirmationModal" max-width="500">
|
||||
<v-form>
|
||||
<v-card class="text-center rounded-xl"
|
||||
:style="{
|
||||
border: `2px solid `
|
||||
}">
|
||||
<div class="flex items-center justify-between py-4 text-2xl font-bold border-b mb-2">
|
||||
<div class="flex-1 text-center">
|
||||
{{$t('contentCard.deletecontenttitle')}}
|
||||
</div>
|
||||
|
||||
<v-btn icon @click="openDeleteConfirmationModal = false" class="ml-auto mr-2" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
|
||||
<div class=" mr-2">
|
||||
{{$t('contentCard.deeletecontentwarning')}}
|
||||
</div>
|
||||
|
||||
<div class="py-2 space-x-3">
|
||||
<v-btn variant="flat"
|
||||
@click="deleteContent()" class=" mt-5">
|
||||
{{$t('general.yes')}}
|
||||
</v-btn>
|
||||
<v-btn variant="outlined"
|
||||
@click="openDeleteConfirmationModal = false" class=" mt-5">
|
||||
{{$t('general.no')}}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onBeforeMount, ref} from 'vue';
|
||||
import {time_ago} from "@/internal_time_ago.js";
|
||||
import MessageList from "@/views/messages/MessageList.vue";
|
||||
import PostMessage from "@/views/messages/PostMessage.vue";
|
||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
||||
import YoutubePlayer from '../YoutubePlayer.vue';
|
||||
import ImageViewer from '../ImageViewer.vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import Reaction from "@/views/contents/Reaction.vue";
|
||||
import {useMessageStore} from "@/stores/messageStore.js";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const openDeleteConfirmationModal = ref(false);
|
||||
const emits = defineEmits(['content-deleted'])
|
||||
|
||||
const contentId = computed(() => props.content.id)
|
||||
const creatorId = computed(() => props.content.createdBy)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const creatorIsCurrentUser = computed(() => authStore.isAuthenticated && authStore.userId === creatorId.value)
|
||||
const messageStore = useMessageStore();
|
||||
const messageCount = ref(0);
|
||||
|
||||
const hasUrls = computed(() => !!props.content.urls && props.content.urls.length > 0);
|
||||
const messagesVisible = ref(false);
|
||||
const messages = ref([]);
|
||||
const hasMessages = computed(() => messages.value.length > 0)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
messageCount.value = await messageStore.fetchMessageCount(contentId.value)
|
||||
})
|
||||
|
||||
function openDeleteConfirmationDialog() {
|
||||
openDeleteConfirmationModal.value = true;
|
||||
}
|
||||
|
||||
function addMessage(newMessage) {
|
||||
messages.value.unshift(newMessage);
|
||||
messagesVisible.value = true;
|
||||
messageCount.value ++;
|
||||
}
|
||||
|
||||
function toggleComments() {
|
||||
messagesVisible.value = !messagesVisible.value;
|
||||
}
|
||||
|
||||
function getComponent(url) {
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
return YoutubePlayer;
|
||||
} else if (url.match(/\.(jpeg|jpg|gif|png)$/)) {
|
||||
return ImageViewer;
|
||||
}
|
||||
}
|
||||
|
||||
function editContent() {
|
||||
console.log('Modifier le contenu');
|
||||
}
|
||||
|
||||
async function deleteContent() {
|
||||
const client = useClient()
|
||||
const response = await client.delete(`/api/contents/${contentId.value}`)
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
emits('content-deleted', contentId.value)
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToContent() {
|
||||
window.location.href = `/content/${props.content.id}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border-color: #EAEBEC;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-carousel-item {
|
||||
padding: 0; /* Supprime tout padding interne */
|
||||
margin: 0; /* Supprime toute marge interne */
|
||||
width: 100vw; /* Assure que chaque item occupe toute la largeur de l'écran */
|
||||
}
|
||||
|
||||
|
||||
.comment-active .v-icon {
|
||||
color: #D63DAB;
|
||||
}
|
||||
</style>
|
||||
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-[calc(100vh-118px)] -mt-2 w-full">
|
||||
|
||||
<div ref="containerRef" class="flex-1 flex items-center justify-center bg-neutral-500 max-h-screen relative">
|
||||
|
||||
<div class="absolute inset-0 z-0 bg-cover bg-center blur-lg pointer-events-none"
|
||||
:style="{ backgroundImage: `url(${currentImage})` }"></div>
|
||||
|
||||
<div class="absolute top-8 left-4 z-20">
|
||||
<v-btn @click="goBack" variant="plain" class="rounded-full text-white w-12 h-12 flex items-center justify-center">
|
||||
<v-icon class="text-black bg-white rounded-full" size="36">mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="multipleImages" class="absolute left-0 top-1/2 transform -translate-y-1/2 z-20">
|
||||
<v-btn @click="previousImage" variant="plain" class="rounded-full bg-gray-800 text-white w-12 h-12">
|
||||
<v-icon size="36">mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-full h-full z-10 overflow-hidden relative">
|
||||
<img
|
||||
:src="currentImage"
|
||||
:style="imageStyle"
|
||||
class="image-content"
|
||||
v-if="isImage(currentImage)"
|
||||
alt="Image"
|
||||
/>
|
||||
<component
|
||||
v-else
|
||||
:is="getComponent(currentImage)"
|
||||
:src="currentImage"
|
||||
class="video-content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="multipleImages" class="absolute right-4 top-1/2 transform -translate-y-1/2 z-10">
|
||||
<v-btn @click="nextImage" variant="plain" class="rounded-full bg-gray-800 text-white w-12 h-12">
|
||||
<v-icon size="36">mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed-width border-l-2 p-6 bg-white overflow-y-auto max-h-screen">
|
||||
<div class="border-b-2 p-6 font-sans space-y-2">
|
||||
<div class="flex flex-row align-center" v-if="data && data.createdByName">
|
||||
<img :src="data.createdByPortraitUrl" class="rounded-full w-9" alt="">
|
||||
<p class="ml-2 capitalize">{{ data.createdByName }}</p>
|
||||
</div>
|
||||
<div v-if="data && data.title" class="font-semibold">{{ data.title }}</div>
|
||||
<div v-if="data && data.description">{{ data.description }}</div>
|
||||
<div class="flex justify-around py-2">
|
||||
<Reaction v-if="data" :content="data"></Reaction>
|
||||
<donation-button v-if="data"></donation-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b-2 p-6">
|
||||
<h2 class="font-sans font-semibold">Commentaires</h2>
|
||||
<message-list :subject-id="contentId" :messages="messages"></message-list>
|
||||
</div>
|
||||
|
||||
<div class="border-b-2 p-6">
|
||||
<post-message :subject-id="contentId" @message-posted="addMessage"></post-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, onBeforeUnmount } from 'vue';
|
||||
import PostMessage from "@/views/messages/PostMessage.vue";
|
||||
import MessageList from "@/views/messages/MessageList.vue";
|
||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
||||
import YoutubePlayer from '../YoutubePlayer.vue';
|
||||
import ImageViewer from '../ImageViewer.vue';
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import { useRoute } from 'vue-router';
|
||||
import Reaction from "@/views/contents/Reaction.vue";
|
||||
import { useMessageStore } from "@/stores/messageStore.js";
|
||||
|
||||
const data = ref(null);
|
||||
const currentImageIndex = ref(0);
|
||||
|
||||
const route = useRoute();
|
||||
const client = useClient();
|
||||
const messageStore = useMessageStore();
|
||||
|
||||
const contentId = computed(() => route.params.contentId);
|
||||
const messages = ref([]);
|
||||
const messageCount = ref(0);
|
||||
const messagesVisible = ref(false);
|
||||
|
||||
const currentImage = computed(() => {
|
||||
if (data.value && data.value.urls) {
|
||||
return data.value.urls[currentImageIndex.value] || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const multipleImages = computed(() => data.value?.urls.length > 1);
|
||||
|
||||
const containerRef = ref(null);
|
||||
const containerWidth = ref(0);
|
||||
const containerHeight = ref(0);
|
||||
|
||||
function updateContainerDimensions() {
|
||||
if (containerRef.value) {
|
||||
containerWidth.value = containerRef.value.offsetWidth;
|
||||
containerHeight.value = containerRef.value.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateContainerDimensions();
|
||||
window.addEventListener('resize', updateContainerDimensions);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateContainerDimensions);
|
||||
});
|
||||
|
||||
const imageStyle = computed(() => {
|
||||
return {
|
||||
maxWidth: `${containerWidth.value}px`,
|
||||
maxHeight: `${containerHeight.value}px`,
|
||||
objectFit: 'contain',
|
||||
};
|
||||
});
|
||||
|
||||
function isImage(url) {
|
||||
return url.match(/\.(jpeg|jpg|gif|png)$/);
|
||||
}
|
||||
|
||||
function getComponent(url) {
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
return YoutubePlayer;
|
||||
} else if (url.match(/\.(jpeg|jpg|gif|png)$/)) {
|
||||
return ImageViewer;
|
||||
}
|
||||
return 'div';
|
||||
}
|
||||
|
||||
const fetchContentData = async (contentId) => {
|
||||
try {
|
||||
const response = await client.get(`/api/contents/${contentId}`);
|
||||
data.value = response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching content: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
function goBack() {
|
||||
window.history.go(-1);
|
||||
}
|
||||
|
||||
function nextImage() {
|
||||
if (data.value?.urls.length > 0) {
|
||||
currentImageIndex.value = (currentImageIndex.value + 1) % data.value.urls.length;
|
||||
}
|
||||
}
|
||||
|
||||
function previousImage() {
|
||||
if (data.value?.urls.length > 0) {
|
||||
currentImageIndex.value = (currentImageIndex.value - 1 + data.value.urls.length) % data.value.urls.length;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleComments() {
|
||||
messagesVisible.value = !messagesVisible.value;
|
||||
}
|
||||
|
||||
function addMessage(newMessage) {
|
||||
messages.value.unshift(newMessage);
|
||||
messagesVisible.value = true;
|
||||
messageCount.value++;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchContentData(contentId.value);
|
||||
messageCount.value = await messageStore.fetchMessageCount(contentId.value);
|
||||
messages.value = await messageStore.fetchMessages(contentId.value);
|
||||
});
|
||||
|
||||
watch(contentId, async (newContentId) => {
|
||||
await fetchContentData(newContentId);
|
||||
messageCount.value = await messageStore.fetchMessageCount(newContentId);
|
||||
messages.value = await messageStore.fetchMessages(newContentId);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.fixed-width {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.v-btn:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.v-btn .v-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col -mt-2">
|
||||
<!-- Titre en haut de l'image -->
|
||||
<div class="bg-white py-4 text-center font-semibold text-lg">
|
||||
<div v-if="data && data.title">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Homemade carousel -->
|
||||
<div class="flex-1 flex items-center justify-center bg-neutral-500 max-h-screen relative">
|
||||
|
||||
<!-- Blur image BG (désactivation des interactions) -->
|
||||
<div class="absolute inset-0 z-0 bg-cover bg-center blur-lg pointer-events-none"
|
||||
:style="{ backgroundImage: `url(${currentImage})` }"></div>
|
||||
|
||||
<!-- back Btn -->
|
||||
<div class="absolute top-8 left-4 z-20">
|
||||
<v-btn @click="goBack" variant="plain"
|
||||
class="rounded-full text-white w-12 h-12 flex items-center justify-center">
|
||||
<v-icon class="text-black bg-white rounded-full" size="36">mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Left arrow collée à gauche -->
|
||||
<div v-if="multipleImages" class="absolute left-0 top-1/2 transform -translate-y-1/2 z-20">
|
||||
<v-btn @click="previousImage" variant="plain" class="rounded-full bg-gray-800 text-white w-12 h-12">
|
||||
<v-icon size="36">mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-full h-full z-10">
|
||||
<img :src="currentImage" alt="Image" class="max-w-full max-h-full object-contain"/>
|
||||
</div>
|
||||
|
||||
<!-- right arrow -->
|
||||
<div v-if="multipleImages" class="absolute right-4 top-1/2 transform -translate-y-1/2 z-10">
|
||||
<v-btn @click="nextImage" variant="plain" class="rounded-full bg-gray-800 text-white w-12 h-12">
|
||||
<v-icon size="36">mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex flex-col p-6 bg-white overflow-y-auto max-h-screen">
|
||||
|
||||
<div class="border-b-2 p-6 font-sans space-y-2">
|
||||
|
||||
<div class="flex flex-row align-center" v-if="data && data.createdByName">
|
||||
<img :src="data.createdByPortraitUrl" class="rounded-full w-9" alt="">
|
||||
<p class="ml-2 capitalize ">{{ data.createdByName }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="data && data.description">
|
||||
Description: {{ data.description }}
|
||||
</div>
|
||||
|
||||
<div v-if="data" class="flex justify-around py-2">
|
||||
<reaction :content="data"></reaction>
|
||||
<donation-button></donation-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="border-b-2 p-6">
|
||||
<h2 class="font-sans font-semibold">Commentaires</h2>
|
||||
<message-list :subject-id="contentId"
|
||||
></message-list>
|
||||
</div>
|
||||
|
||||
<div class="border-b-2 p-6">
|
||||
<post-message :subject-id="contentId"
|
||||
></post-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, computed, onMounted, watch} from 'vue';
|
||||
import PostMessage from "@/views/messages/PostMessage.vue";
|
||||
import MessageList from "@/views/messages/MessageList.vue";
|
||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useRoute} from 'vue-router';
|
||||
import Reaction from "@/views/contents/Reaction.vue";
|
||||
|
||||
const data = ref(null);
|
||||
const currentImageIndex = ref(0);
|
||||
|
||||
const route = useRoute();
|
||||
const client = useClient();
|
||||
|
||||
const contentId = computed(() => {
|
||||
return route.params.contentId;
|
||||
});
|
||||
|
||||
const currentImage = computed(() => {
|
||||
if (data.value && data.value.urls) {
|
||||
return data.value.urls[currentImageIndex.value] || '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
// Calculer si on a plus d'une image
|
||||
const multipleImages = computed(() => {
|
||||
if (data.value && data.value.urls) {
|
||||
return data.value.urls.length > 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const fetchContentData = async (contentId) => {
|
||||
try {
|
||||
const response = await client.get(`/api/contents/${contentId}`);
|
||||
data.value = response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching content: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
function goBack() {
|
||||
window.history.go(-1);
|
||||
}
|
||||
|
||||
function nextImage() {
|
||||
if (data.value?.urls.length > 0) {
|
||||
currentImageIndex.value = (currentImageIndex.value + 1) % data.value.urls.length;
|
||||
}
|
||||
}
|
||||
|
||||
function previousImage() {
|
||||
if (data.value?.urls.length > 0) {
|
||||
currentImageIndex.value = (currentImageIndex.value - 1 + data.value.urls.length) % data.value.urls.length;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchContentData(contentId.value);
|
||||
});
|
||||
|
||||
watch(contentId, (newContentId) => {
|
||||
fetchContentData(newContentId);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fixed-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.v-btn:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.v-btn .v-icon {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div v-if="brandingStore.value.loading">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</div>
|
||||
<div v-else>
|
||||
<content-list :creator-id="brandingStore.value.id"
|
||||
></content-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script async setup>
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import ContentList from "@/views/contents/ContentList.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
const brandingStore = useBrandingStore()
|
||||
const router = useRouter();
|
||||
|
||||
const createHtmlContent = () => {
|
||||
router.push('/content/editor');
|
||||
}
|
||||
|
||||
const createContent = () => {
|
||||
router.push('/content/post');
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,89 +0,0 @@
|
||||
<script setup>
|
||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
||||
import { ref } from "vue";
|
||||
|
||||
const branding = useBrandingStore();
|
||||
const menu = ref(false); // C'est pour le menu déroulant!
|
||||
|
||||
// Fonction pour convertir une couleur hexadécimale en RGB afin d'appliquer la transparence avec nos couleurs du backend hex a rgb
|
||||
function hexToRgb(hex) {
|
||||
const bigint = parseInt(hex.replace('#', ''), 16);
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center">
|
||||
<!-- ExclusiveCard -->
|
||||
<div
|
||||
class="rounded-lg w-[290px] h-[380px] relative bg-hSurface"
|
||||
:style="{
|
||||
boxShadow: '0 10px 10px rgba(0, 0, 0, 0.3)',
|
||||
borderColor: `rgba(${hexToRgb('--h-secondary')}, 0.4)`,
|
||||
borderWidth: '1px',
|
||||
}"
|
||||
>
|
||||
<!-- Conteneur pour aligner le titre et le bouton -->
|
||||
<div
|
||||
class="flex items-center justify-between py-2 px-3 text-hOnPrimary"
|
||||
>
|
||||
<div class="text-md">Comment créer un logo</div>
|
||||
|
||||
<!-- Bouton à trois points avec menu déroulant -->
|
||||
<v-menu v-model="menu" activator="parent" offset-y>
|
||||
<template #activator="{ props }">
|
||||
<button
|
||||
v-bind="props"
|
||||
class="text-gray-600 text-hOnPrimary"
|
||||
>
|
||||
<i class="mdi mdi-dots-vertical text-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
<v-list class="bg-hSecondary text-hOnSecondary">
|
||||
<v-list-item title="Modifier" @click="modifier" />
|
||||
<v-list-item title="Effacer" @click="effacer" />
|
||||
<v-list-item title="Reporter" @click="reporter" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div class="relative h-[170px] overflow-hidden">
|
||||
<img
|
||||
src="/images/hutopymedia/banners/hutopyul.png"
|
||||
class="w-full h-full object-cover blur-md"
|
||||
alt="image"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i class="mdi mdi-lock text-7xl p-2 rounded-full text-hSecondary border-2 border-solid border-hSecondary"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end pa-2 px-4 text-hOnPrimary">
|
||||
14-05-2024
|
||||
</div>
|
||||
|
||||
<div class="text-justify px-4 text-md text-hOnPrimary">
|
||||
Tutoriel sur comment s'assurer d'avoir un logo unique et percutant
|
||||
qui se démarque de la concurrence.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<script>
|
||||
function modifier() {
|
||||
console.log("Modifier l'élément");
|
||||
}
|
||||
function effacer() {
|
||||
console.log("Effacer l'élément");
|
||||
}
|
||||
function reporter() {
|
||||
console.log("Reporter l'élément");
|
||||
}
|
||||
</script>
|
||||
@@ -1,169 +0,0 @@
|
||||
<template>
|
||||
<div class="overflow-hidden relative" @wheel="handleScroll">
|
||||
<!-- Container that holds all the posts and permet le défilement -->
|
||||
<div class="relative h-[1000px] max-h-[1000px] overflow-hidden p-4">
|
||||
<div class="transition-transform duration-500" :style="{ transform: `translateY(-${scrollPosition}px)` }">
|
||||
<!-- Grille avec colonnes dynamiques basées sur la largeur -->
|
||||
<div class="grid gap-3 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 min-w-[250px]">
|
||||
<div v-for="(item, index) in contenuexclusif" :key="index"
|
||||
class="my-1 text-white rounded-lg w-full border-2 shadow h-[380px] hover-card relative overflow-hidden bg-hPrimary text-hOnPrimary border-hSecondary">
|
||||
<div class="flex justify-center items-center">
|
||||
</div>
|
||||
<div>
|
||||
<img :src="item.photo" alt="photo" class="w-full h-auto max-h-[170px] object-cover" />
|
||||
|
||||
<!-- Section du nombre de clics et du bouton d'édition -->
|
||||
<div class="flex flex-row justify-between items-center p-2">
|
||||
<div class="flex items-center">
|
||||
<p class="text-xs">{{ item.date }}</p>
|
||||
<p class="text-xs px-2">|</p>
|
||||
<p class="text-xs">200 clicks</p>
|
||||
</div>
|
||||
<!-- Bouton pour éditer le contenu à droite -->
|
||||
<v-btn class="" variant="plain" @click="editCard(item)">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<p class="text-md p-4">{{ item.title }}</p>
|
||||
<!-- Section des étoiles, fixée dans le coin inférieur droit -->
|
||||
<div v-if="item.rating" class="stars flex justify-end p-2 absolute bottom-0 right-0">
|
||||
<!-- Génération dynamique des étoiles -->
|
||||
<span v-for="star in 5" :key="star" class="text-yellow-500">
|
||||
<v-icon v-if="star <= item.rating">mdi-star</v-icon>
|
||||
<v-icon v-else>mdi-star-outline</v-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, defineProps } from 'vue';
|
||||
|
||||
function hexToRgb(hex) {
|
||||
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
function getRGB(hexColor) {
|
||||
const rgb = hexToRgb(hexColor);
|
||||
return `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
||||
}
|
||||
|
||||
const contenuexclusif = ref([
|
||||
{ title: 'Créer un site web moderne', description: 'Un guide pour concevoir un site qui attire l\'attention et se démarque.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 2, date: '2024-09-19' },
|
||||
{ title: ' Les secrets d’un logo réussiLes secrets d’un logo réussiLes secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
{ title: 'Les secrets d’un logo réussi', description: 'Découvrez les astuces pour un logo mémorable et distinctif.', photo: '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png', rating: 4, date: '2024-09-19' },
|
||||
// autres objets...
|
||||
]);
|
||||
|
||||
const scrollPosition = ref(0);
|
||||
const cardHeight = 320;
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function handleScroll(event) {
|
||||
event.preventDefault();
|
||||
const scrollSpeed = 100;
|
||||
scrollPosition.value += event.deltaY > 0 ? scrollSpeed : -scrollSpeed;
|
||||
|
||||
const totalRows = Math.ceil(contenuexclusif.value.length / getCurrentCols());
|
||||
const visibleRows = 1000 / cardHeight;
|
||||
const maxScrollPosition = totalRows * cardHeight - visibleRows * cardHeight + 360;
|
||||
|
||||
if (scrollPosition.value < 0) {
|
||||
scrollPosition.value = 0;
|
||||
} else if (scrollPosition.value > maxScrollPosition) {
|
||||
scrollPosition.value = maxScrollPosition;
|
||||
}
|
||||
}
|
||||
|
||||
const gridColsClass = computed(() => {
|
||||
const width = window.innerWidth;
|
||||
if (width >= 1200) {
|
||||
return 'grid-cols-4';
|
||||
} else if (width >= 900) {
|
||||
return 'grid-cols-3';
|
||||
} else if (width >= 600) {
|
||||
return 'grid-cols-2';
|
||||
} else {
|
||||
return 'grid-cols-1';
|
||||
}
|
||||
});
|
||||
|
||||
function getCurrentCols() {
|
||||
const width = window.innerWidth;
|
||||
if (width >= 1200) {
|
||||
return 4;
|
||||
} else if (width >= 900) {
|
||||
return 3;
|
||||
} else if (width >= 600) {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function editCard(item) {
|
||||
console.log(`Editing card: ${item.title}`);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
gridColsClass.value = getCurrentCols();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hover-card {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.hover-card:hover {
|
||||
transform: scale(1.03); /* Effet de hover restauré */
|
||||
}
|
||||
|
||||
.stars {
|
||||
font-size: 18px; /* Ajustez la taille des icônes */
|
||||
}
|
||||
|
||||
.stars {
|
||||
position: absolute; /* Fixe les étoiles au bas à droite */
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<script setup>
|
||||
import {useSubscriptionStore} from "@/stores/subscriptionStore.js";
|
||||
import {computed, ref} from "vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter()
|
||||
const brandingStore = useBrandingStore()
|
||||
const subscriptionStore = useSubscriptionStore()
|
||||
|
||||
const isSubscribe = computed(() => !subscriptionStore.isSubscribeTo(brandingStore.value.id));
|
||||
|
||||
function subscribeToCreator() {
|
||||
const target = `@${brandingStore.currentBrand}/subscription`;
|
||||
router.push(target)
|
||||
}
|
||||
|
||||
// Référence pour contrôler l'affichage du modal
|
||||
const showUnsubscribeModal = ref(false);
|
||||
|
||||
function unsubscribeFromCreator() {
|
||||
subscriptionStore.unsubscribeFrom(brandingStore.value.id);
|
||||
// Fermer le modal après désabonnement
|
||||
showUnsubscribeModal.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="isSubscribe">
|
||||
<v-btn
|
||||
:style="{
|
||||
width: '150px',
|
||||
height: '28px',
|
||||
backgroundColor: '--h-secondary',
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '10px 24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'background-color 0.3s ease'
|
||||
}"
|
||||
@click="subscribeToCreator"
|
||||
>
|
||||
{{ $t('subscribebutton.subscribe') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-btn
|
||||
:style="{
|
||||
width: '150px',
|
||||
height: '28px',
|
||||
backgroundColor: '--h-secondary',
|
||||
color: 'white',
|
||||
borderRadius: '0 8px 8px 0',
|
||||
padding: '10px 24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'background-color 0.3s ease'
|
||||
}"
|
||||
@click="showUnsubscribeModal = true"
|
||||
>
|
||||
<div>{{ $t('subscribebutton.unsubscribe') }}</div>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-dialog v-model="showUnsubscribeModal" max-width="500">
|
||||
<v-card class="text-center rounded-xl border-2 border-solid border-hSecondary">
|
||||
|
||||
<div class="flex items-center justify-between py-4 text-2xl font-bold border-b mb-2">
|
||||
<div class="flex-1 text-center">
|
||||
Déabonnement
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card-title>Confirmation</v-card-title>
|
||||
<v-card-text>Êtes-vous sûr de vouloir vous désabonner ?</v-card-text>
|
||||
<v-card-actions class="justify-center px-4 pb-4">
|
||||
<v-btn text class="flex-grow-1" variant="outlined"
|
||||
:style="{ backgroundColor: 'rgba(255, 255, 255, 0.1)', color: 'rgba(0, 0, 0, 0.4)' }"
|
||||
@click="unsubscribeFromCreator">Oui
|
||||
</v-btn>
|
||||
|
||||
<v-btn class="flex-grow-1 border-hSecondary text-hOnSecondary"
|
||||
variant="outlined"
|
||||
@click="showUnsubscribeModal = false">
|
||||
Non
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<script setup>
|
||||
import {useSubscriptionStore} from "@/stores/subscriptionStore.js";
|
||||
|
||||
const subscriptionStore = useSubscriptionStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<template v-if="Object.keys(subscriptionStore.subscriptions).length > 0">
|
||||
<template v-for="subscription in subscriptionStore.subscriptions">
|
||||
|
||||
<RouterLink class="capitalize" :to="`/@${subscription.creatorName}`">
|
||||
|
||||
<div class="flex items-center content-center font-sans font-semibold pt-2 ">
|
||||
<img
|
||||
:src="subscription.creatorPortraitUrl"
|
||||
alt="Profile Image"
|
||||
class="rounded-full mx-2"
|
||||
width="32px"
|
||||
height="32px">
|
||||
{{ subscription.creatorName }}
|
||||
</div>
|
||||
|
||||
</RouterLink>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span>No subscriptions</span>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script setup>
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import {ref, onMounted} from 'vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const brandingStore = useBrandingStore();
|
||||
|
||||
const tiers = ref([]);
|
||||
|
||||
// Fetch tiers from API
|
||||
async function fetchTiers() {
|
||||
const client = useClient()
|
||||
const response = await client.get(
|
||||
`/api/membership/tiers/${brandingStore.value.id}`
|
||||
);
|
||||
tiers.value = response.data;
|
||||
}
|
||||
|
||||
// Fetch tiers when the component is mounted
|
||||
onMounted(() => {
|
||||
fetchTiers();
|
||||
});
|
||||
|
||||
const route = useRoute()
|
||||
const baseUrl = window.location.origin;
|
||||
const creatorSlug = route.params.creator_slug || route.path.split('/')[1];
|
||||
const successUrl = `${baseUrl}/${creatorSlug}/content`
|
||||
const cancelledUrl = `${baseUrl}/${creatorSlug}`
|
||||
|
||||
async function doSubscribe(tier) {
|
||||
try {
|
||||
const client = useClient()
|
||||
const response = await client.post(
|
||||
`/api/membership/subscribe`,
|
||||
{
|
||||
creatorId: brandingStore.value.id,
|
||||
tierId: tier.id,
|
||||
checkoutSuccessUrl: successUrl, // TODO: ensure the success-url will insert subscription
|
||||
checkoutCancelledUrl: cancelledUrl
|
||||
})
|
||||
|
||||
window.location.href = response.data.stripeCheckoutUrl;
|
||||
} catch (error) {
|
||||
console.error("Error loading subscriptions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container class="d-flex justify-center">
|
||||
<v-row justify="center">
|
||||
|
||||
<v-col
|
||||
:cols="12 / Math.min(tiers.length, 3)"
|
||||
md="4"
|
||||
v-for="tier in tiers"
|
||||
:key="tier.id"
|
||||
>
|
||||
<v-btn @click="doSubscribe(tier)" variant="text">
|
||||
<div class="bg-white shadow-2xl rounded-2xl">
|
||||
<v-img src="/images/hutopymedia/loginpage/loginhutopy.png" class="rounded-t-2xl"></v-img>
|
||||
<div class="pa-6" :style="[Primary, onPrimary]">
|
||||
<v-card-title class="text-h4 text-center py-4 ">{{ tier.name }}</v-card-title>
|
||||
<div class="text-justify">{{ tier.description }}</div>
|
||||
</div>
|
||||
|
||||
<v-card-text class="text-center rounded-b-2xl" :style="[secondaryColor, onSecondaryColor]">
|
||||
<span class="text-h5">{{ tier.price }} $ / par mois</span>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,121 +0,0 @@
|
||||
<template>
|
||||
<v-container class="mt-10 bg-gray-100 py-10 rounded-lg shadow-lg border border-fuchsia-500 mb-15">
|
||||
<div class="flex justify-center text-6xl mb-12 font-sans font-weight-bold">Portefeuille</div>
|
||||
<div class="flex justify-between mb-4">
|
||||
<div class="text-left">
|
||||
<span class="font-bold">Montant Total : {{ formattedBalance }}</span>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="font-bold">Transactions total : {{ transactionCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="formattedTransactions"
|
||||
class="elevation-1 text-black"
|
||||
:items-per-page="5"
|
||||
show-group-by
|
||||
>
|
||||
</v-data-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<v-btn icon @click="openModal">
|
||||
<v-icon class="text-[#A30E79]">mdi-information</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-dialog v-model="isModalOpen" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Tarification
|
||||
</v-card-title>
|
||||
<v-card-text class="scrollable-content">
|
||||
Découvrez Hutopy, l'endroit où la valorisation de votre travail atteint son apogée. Avec une commission réduite à seulement 9 %, notre engagement envers votre succès est palpable. Chaque pourcentage prélevé est réinvesti avec soin pour catalyser votre croissance : du développement de fonctionnalités innovantes à la maintenance d'une infrastructure technologique de pointe, en passant par un support utilisateur de premier ordre. Notre objectif ? Amplifier votre expansion et garantir une expérience utilisateur sans précédent.
|
||||
Pour chaque transaction, un frais minime assure la sécurité et la fiabilité de vos paiements, grâce à un partenaire de confiance à la renommée mondiale. Ce dernier sécurise pour des milliards en transaction chaque année pour une diversité d'entreprises, allant des startups dynamiques aux conglomérats établis. Ce gage de sécurité est disponible pour une modique somme : 2,9 % plus 0,30 $ par transaction, une petite contribution pour la tranquillité d'esprit et la protection de vos revenus.
|
||||
Notre modèle tarifaire a été pensé dans un esprit de simplicité et de transparence, avec l'ambition ultime d'optimiser vos gains. Chez Hutopy, la notion de partenariat prend tout son sens : votre épanouissement est au cœur de nos préoccupations. Bénéficiez d'une plateforme qui élargit votre horizon créatif et entrepreneurial, tout en vous assurant que vos intérêts sont précieusement gardés.
|
||||
Hutopy est plus qu'une plateforme ; c'est une communauté où la transformation de la passion en profit devient réalité, grâce au soutien indéfectible d'une équipe dévouée à enrichir votre parcours. Nous vous invitons à nous rejoindre pour explorer ensemble les avenues de succès que nous pouvons emprunter ensemble, tout en vous garantissant une part conséquente de vos revenus. Embarquez dans une aventure où votre présence en ligne ne connaît pas de limites, soutenue par Hutopy, votre allié dans la quête du succès.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text class="ml-auto" @click="isModalOpen = false">Fermer</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<div class="flex justify-center mt-4 ">
|
||||
<v-btn text class="transparent-btn text-lg px-12" @click="navigateToHome">Retour</v-btn>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script async setup>
|
||||
import { onBeforeMount, ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
|
||||
const userProfileStore = useUserProfileStore();
|
||||
const router = useRouter();
|
||||
|
||||
const userTransactions = ref([]);
|
||||
const totalBalance = ref("");
|
||||
const isModalOpen = ref(false);
|
||||
|
||||
const formattedTransactions = computed(() => {
|
||||
return userTransactions.value.map(transaction => ({
|
||||
...transaction,
|
||||
created: transaction.created.split('T')[0]
|
||||
}));
|
||||
});
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
const balance = totalBalance.value.toString();
|
||||
return `${balance} $`
|
||||
});
|
||||
|
||||
const transactionCount = computed(() => userTransactions.value.length);
|
||||
|
||||
onBeforeMount( () => {
|
||||
try {
|
||||
userTransactions.value = userProfileStore.value.userTransactions;
|
||||
totalBalance.value = userProfileStore.value.totalBalance;
|
||||
} catch (error) {
|
||||
navigateToHome();
|
||||
}
|
||||
});
|
||||
|
||||
const headers = ref([
|
||||
{ title: 'Montant', value: 'amount', width: '20%', key: "amount" },
|
||||
{ title: 'Date', value: 'created', width: '20%', key: "created" },
|
||||
{ title: 'Message', value: 'tipMessage', width: '60%' }
|
||||
]);
|
||||
|
||||
const navigateToHome = () => {
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scrollable-content {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scrollable-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollable-content::-webkit-scrollbar-thumb {
|
||||
background-color: #A30E79;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.scrollable-content::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #a21caf;
|
||||
}
|
||||
|
||||
.transparent-btn {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-column py-2">
|
||||
<div class="flex flex-row full">
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div>
|
||||
<div class="content-center flex flex-row">
|
||||
<img
|
||||
:src="message.createdByPortraitUrl ?? '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="32px"
|
||||
height="32px"
|
||||
/>
|
||||
<span class="font-semibold font-sans mr-2 capitalize ml-2">
|
||||
{{ message.createdByName }}
|
||||
</span>
|
||||
|
||||
<v-tooltip :text="new Date(message.createdAt).toLocaleString()">
|
||||
<template v-slot:activator="{ props }">
|
||||
<span v-bind="props" class="text-sm-caption text-gray-700 mt-1 ">
|
||||
{{ time_ago(message.createdAt) }}
|
||||
</span>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-menu class="ml-auto" v-if="messageAuthorIsCurrentUser">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" icon v-bind="props">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="editMessage(message)">
|
||||
<v-list-item-title>{{ $t('message.edit') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteMessage()">
|
||||
<v-list-item-title>{{ $t('message.delete') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div class="font-sans message-content">
|
||||
<p class="pb-2" v-if="!isEditMessage"> {{ message.value }}</p>
|
||||
|
||||
<div v-if="isEditMessage" class="flex flex-row">
|
||||
<v-textarea
|
||||
variant="outlined"
|
||||
v-model="editMessageValue"
|
||||
rows="1"
|
||||
auto-grow
|
||||
class="flex-1 mt-3"
|
||||
@keyup.enter="acceptChanges"
|
||||
></v-textarea>
|
||||
|
||||
<div class="flex flex-row px-2 space-y-1 align-center">
|
||||
<v-btn variant="plain" @click="cancel">
|
||||
<v-icon class="rounded-full">mdi-cancel</v-icon>
|
||||
</v-btn>
|
||||
<v-btn variant="plain" @click="acceptChanges">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<must-be-logged v-model="loginModal"
|
||||
message="Vous devez être connecté pour supprimer ou modifier un commentaire."></must-be-logged>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch, onMounted, onBeforeUnmount, computed} from "vue";
|
||||
import {time_ago} from "@/internal_time_ago.js";
|
||||
import MustBeLogged from "@/views/MustBeLogged.vue";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const isEditMessage = ref(false);
|
||||
const editMessageValue = ref("");
|
||||
const originalMessageValue = ref("");
|
||||
const loginModal = ref(false);
|
||||
const authStore = useAuthStore();
|
||||
const client = useClient();
|
||||
const messageAuthorId = computed(() => props.message.createdBy)
|
||||
const messageAuthorIsCurrentUser = computed(() => authStore.isAuthenticated && authStore.userId === messageAuthorId.value)
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['message-deleted']);
|
||||
|
||||
function editMessage(message) {
|
||||
isEditMessage.value = true;
|
||||
originalMessageValue.value = message.value;
|
||||
editMessageValue.value = message.value;
|
||||
}
|
||||
|
||||
const acceptChanges = async () => {
|
||||
props.message.value = editMessageValue.value;
|
||||
isEditMessage.value = false;
|
||||
|
||||
console.log('Update message', props.message.value);
|
||||
if (!authStore.isAuthenticated) {
|
||||
loginModal.value = true;
|
||||
} else {
|
||||
try {
|
||||
await client.post(`/api/messages/update`, {
|
||||
"id": props.message.id,
|
||||
"subjectId": props.message.subjectId,
|
||||
"message": props.message.value
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`post api/message/update : ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
editMessageValue.value = originalMessageValue.value;
|
||||
isEditMessage.value = false;
|
||||
}
|
||||
|
||||
const deleteMessage = async () => {
|
||||
console.log('Delete message', props.message);
|
||||
if (!authStore.isAuthenticated) {
|
||||
loginModal.value = true;
|
||||
} else {
|
||||
try {
|
||||
await client.delete(`/api/messages/${props.message.id}`)
|
||||
emits('message-deleted', {
|
||||
"id": props.message.id
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`delete api/message : ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
watch(isEditMessage, (newValue) => {
|
||||
if (newValue) {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (isEditMessage.value) {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.content-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -1,96 +0,0 @@
|
||||
<template>
|
||||
<v-infinite-scroll
|
||||
:items="messages"
|
||||
:onLoad="fetchMessages"
|
||||
mode="manual"
|
||||
class="justify-items-center"
|
||||
>
|
||||
<template v-for="message in messages" :key="message">
|
||||
<div class="border-b">
|
||||
<message :message="message"
|
||||
@message-deleted="(messageId) => handleDeleteMessage(messageId)"
|
||||
></message>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:load-more="{ props }">
|
||||
<v-btn size="small" variant="outlined" v-bind="props">
|
||||
Voir plus de commentaires
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:empty>
|
||||
Il n'y a pas plus de commentaires
|
||||
</template>
|
||||
|
||||
<template v-slot:error>
|
||||
<v-alert type="error">{{ errorMessage }}</v-alert>
|
||||
</template>
|
||||
</v-infinite-scroll>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onBeforeMount} from 'vue';
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import Message from "@/views/messages/Message.vue";
|
||||
|
||||
const props = defineProps({
|
||||
subjectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const errorMessage = ref(null);
|
||||
let last_id = null;
|
||||
const client = useClient();
|
||||
const messages = ref(props.messages);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
if (props.subjectId == null) return;
|
||||
await fetchMessages({
|
||||
page_size: 2,
|
||||
done: function (status) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchMessages({done, page_size = 10}) {
|
||||
if (props.subjectId == null) return
|
||||
|
||||
try {
|
||||
let uri = `/api/messages/${props.subjectId}?page_size=${page_size}`;
|
||||
if (last_id !== null) uri = uri + `&last_id=${last_id}`;
|
||||
|
||||
const response = await client.get(uri);
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const messageCount = response.data.messages.length;
|
||||
|
||||
if (messageCount > 0) {
|
||||
messages.value.push(...response.data.messages);
|
||||
const [last_content] = response.data.messages.slice(-1);
|
||||
last_id = last_content.id;
|
||||
}
|
||||
|
||||
if (messageCount < page_size) {
|
||||
done('empty');
|
||||
} else {
|
||||
done('ok');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch messages", error);
|
||||
errorMessage.value = error.message || "Failed to fetch messages";
|
||||
done('error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteMessage(message) {
|
||||
messages.value = messages.value.filter(item => item.id !== message.id);
|
||||
}
|
||||
</script>
|
||||
@@ -1,113 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-column">
|
||||
<div class="flex flex-row items-center ">
|
||||
<img :src="userProfileStore.portraitUrl" alt="Profile Image" class="rounded-full mr-2" width="32px" height="32px">
|
||||
<div class="flex-grow">
|
||||
<div class="flex flex-row bg-gray-100 rounded-2xl">
|
||||
<v-textarea
|
||||
v-model="value"
|
||||
density="compact"
|
||||
variant="underlined"
|
||||
:placeholder="$t('message.yourcomment')"
|
||||
hide-details
|
||||
auto-grow
|
||||
rows="1"
|
||||
maxlength="1024"
|
||||
class="pr-1 ml-6 flex-grow"
|
||||
@keydown.enter.prevent="publish"
|
||||
|
||||
>
|
||||
</v-textarea>
|
||||
<div class="flex flex-col justify-center">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="publish"
|
||||
>
|
||||
<v-icon>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end items-center mt-1">
|
||||
<div v-if="value.length < 1024" class="text-gray-500 text-sm">{{ value.length }}/1024</div>
|
||||
<div v-if="value.length >= 1024" class="text-red-500 text-sm">{{ value.length }}/1024</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<must-be-logged v-model="loginModal"
|
||||
message="Vous devez être connecté pour ajouter un commentaire."
|
||||
></must-be-logged>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
import {v7} from 'uuid'
|
||||
import {useClient} from '@/plugins/api.js'
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js"
|
||||
import {useAuthStore} from "@/stores/authStore.js"
|
||||
import MustBeLogged from "@/views/MustBeLogged.vue";
|
||||
|
||||
const props = defineProps({
|
||||
subjectId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['message-posted'])
|
||||
|
||||
const loginModal = ref(false);
|
||||
const client = useClient()
|
||||
const value = ref("")
|
||||
const userProfileStore = useUserProfileStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const publish = async () => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
loginModal.value = true;
|
||||
} else {
|
||||
try {
|
||||
const messageId = v7()
|
||||
await client.post(`/api/messages/`, {
|
||||
"id": messageId,
|
||||
"subjectId": props.subjectId,
|
||||
"message": value.value
|
||||
})
|
||||
emits('message-posted', {
|
||||
"id": messageId,
|
||||
"subjectId": props.subjectId,
|
||||
"createdBy": userProfileStore.user.id,
|
||||
"createdByName": userProfileStore.alias,
|
||||
"createdByPortraitUrl": userProfileStore.portraitUrl,
|
||||
"createdAt": new Date(Date.now()).toISOString(),
|
||||
"value": value.value,
|
||||
"parentId": null
|
||||
})
|
||||
value.value = ''
|
||||
} catch (error) {
|
||||
console.error(`post api/message : ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-red-500 {
|
||||
color: #f56565;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="screenSize === 'sm'" class="size-code bg-blue-500 text-white">
|
||||
sm
|
||||
</div>
|
||||
|
||||
<div v-if="screenSize === 'md'" class="size-code bg-green-500 text-white">
|
||||
md
|
||||
</div>
|
||||
|
||||
<div v-if="screenSize === 'lg'" class="size-code bg-yellow-500 text-black">
|
||||
lg
|
||||
</div>
|
||||
|
||||
<div v-if="screenSize === 'xl'" class="size-code bg-red-500 text-white">
|
||||
xl
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
screenSize: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.detectScreenSize();
|
||||
window.addEventListener('resize', this.detectScreenSize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.detectScreenSize);
|
||||
},
|
||||
methods: {
|
||||
detectScreenSize() {
|
||||
const width = window.innerWidth;
|
||||
|
||||
if (width < 640) {
|
||||
this.screenSize = 'sm';
|
||||
} else if (width >= 640 && width < 1024) {
|
||||
this.screenSize = 'md';
|
||||
} else if (width >= 1024 && width < 1280) {
|
||||
this.screenSize = 'lg';
|
||||
} else {
|
||||
this.screenSize = 'xl';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.size-code {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user