Add 'frontend/' from commit 'c070c0315d66a44154ab7d9f9ea6c211a15f4dba'
git-subtree-dir: frontend git-subtree-mainline:205a3bd14bgit-subtree-split:c070c0315d
This commit is contained in:
134
frontend/src/views/CTA01.vue
Normal file
134
frontend/src/views/CTA01.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<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>
|
||||
39
frontend/src/views/LoginView.vue
Normal file
39
frontend/src/views/LoginView.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div v-if="isMobileView" class="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6 text-center">
|
||||
<!-- Image -->
|
||||
<img src="/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png" alt="Image" class="w-64 h-64 rounded-full mb-4 border" />
|
||||
|
||||
<!-- Message -->
|
||||
<div class="text-lg text-gray-700 mt-8">
|
||||
<p class="font-semibold mb-2">Pour vous connecter et modifier votre page, veuillez utiliser un appareil avec un écran plus large, comme un ordinateur.</p>
|
||||
<p>Pour le moment, l'expérience sur téléphone n'est pas encore complétée.</p>
|
||||
<p class="mt-4 font-bold">Désolé de l'inconvénient.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-start justify-center py-2">
|
||||
<div class="max-w-[600px] mt-[10%] ">
|
||||
<img class="rounded-2xl"
|
||||
src="/images/hutopymedia/loginpage/loginhutopy.png"
|
||||
alt="hutopy login">
|
||||
|
||||
<login-form></login-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LoginForm from "@/views/main/LoginForm.vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import {ref, watch} from "vue";
|
||||
|
||||
const { smAndDown } = useDisplay();
|
||||
const isMobileView = ref(smAndDown.value);
|
||||
|
||||
watch(smAndDown, (newVal) => {
|
||||
isMobileView.value = newVal;
|
||||
});
|
||||
|
||||
</script>
|
||||
42
frontend/src/views/MustBeLogged.vue
Normal file
42
frontend/src/views/MustBeLogged.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import LoginForm from "@/views/main/LoginForm.vue";
|
||||
|
||||
const isOpen = defineModel();
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSuccess = () => {
|
||||
console.log('handleSuccess triggered');
|
||||
isOpen.value = false;
|
||||
}
|
||||
|
||||
const handleFailure = () => {
|
||||
console.error('Login failed');
|
||||
}
|
||||
|
||||
const closeModal = async () => {
|
||||
console.log('closeModal triggered');
|
||||
isOpen.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="isOpen" max-width="400">
|
||||
<v-card>
|
||||
<div class="flex flex-col items-center">
|
||||
<v-img src="/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png" class="w-50"></v-img>
|
||||
<LoginForm :onSuccess="handleSuccess" :onFailure="handleFailure"></LoginForm>
|
||||
<v-card-text>{{ message }}</v-card-text>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text @click="closeModal">Fermer</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
118
frontend/src/views/PaymentCompleted.vue
Normal file
118
frontend/src/views/PaymentCompleted.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<v-container class="py-10">
|
||||
<v-row class="d-flex flex-column align-center">
|
||||
<v-col cols="10">
|
||||
<v-card
|
||||
class="elevation-3"
|
||||
style="background-color: white; border-radius: 12px"
|
||||
>
|
||||
<!-- Title Section -->
|
||||
<v-card-title class="text-center text-h4 font-weight-bold mb-4">
|
||||
{{ $t('paymentConfirmation.success.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<!-- Check Icon -->
|
||||
<v-card-text class="text-center mb-4">
|
||||
<v-icon size="120" color="success">mdi-check-circle</v-icon>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Thank You Message -->
|
||||
<v-card-text class="text-center mb-4">
|
||||
<p class="text-h6">
|
||||
{{ $t('paymentConfirmation.success.message') }}
|
||||
<span class="text-h5 font-weight-bold" v-if="creatorUserName">{{
|
||||
creatorUserName
|
||||
}}</span>
|
||||
<span class="text-h6" v-else>{{
|
||||
$t('paymentConfirmation.success.usernameDefault')
|
||||
}}</span>
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Email Input and Receipt Button -->
|
||||
<v-card-text class="text-center mb-4">
|
||||
<p class="text-h6">
|
||||
{{ $t('paymentConfirmation.success.receipt') }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Continue Button -->
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="text-white px-5 py-3"
|
||||
@click="router.push({ path: `/@${creatorUserName}` })"
|
||||
>
|
||||
{{ $t('paymentConfirmation.success.continue') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Error Snackbar -->
|
||||
<v-snackbar v-model="errorSnackBar" color="red darken-1">
|
||||
Aucun reçu trouvé pour cet email.
|
||||
<template v-slot:actions>
|
||||
<v-btn color="white" text @click="errorSnackBar = false">Fermer</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const client = useClient();
|
||||
|
||||
const creatorId = route.params.creatorId;
|
||||
const creatorUserName = ref('');
|
||||
const email = ref('');
|
||||
const errorSnackBar = ref(false);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
const response = await client.get(`/api/creators/${creatorId}`);
|
||||
creatorUserName.value = response.data.name;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch creator data:', error);
|
||||
}
|
||||
});
|
||||
|
||||
async function getReceipt() {
|
||||
try {
|
||||
const response = await client.get(
|
||||
`/api/Stripe/GetMyLastReceipt?CreatorId=${creatorId}&Email=${email.value}`
|
||||
);
|
||||
const receiptUrl = response.data.receiptUrl;
|
||||
|
||||
if (!receiptUrl) {
|
||||
errorSnackBar.value = true;
|
||||
} else {
|
||||
window.open(receiptUrl, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch receipt:', error);
|
||||
errorSnackBar.value = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-container {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
84
frontend/src/views/PaymentFailed.vue
Normal file
84
frontend/src/views/PaymentFailed.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-container class="py-10">
|
||||
<v-row class="d-flex flex-column align-center">
|
||||
<v-col cols="10">
|
||||
<v-card
|
||||
class="elevation-3"
|
||||
style="background-color: white; border-radius: 12px"
|
||||
>
|
||||
<!-- Title Section -->
|
||||
<v-card-title
|
||||
class="text-center text-h4 font-weight-bold mb-4 text-danger"
|
||||
>
|
||||
{{ $t('paymentConfirmation.failure.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<!-- Cancel Icon -->
|
||||
<v-card-text class="text-center mb-4">
|
||||
<v-icon size="120" color="error">mdi-close-circle</v-icon>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Message -->
|
||||
<v-card-text class="text-center mb-4">
|
||||
<p class="text-h6">
|
||||
{{ $t('paymentConfirmation.failure.message') }}
|
||||
</p>
|
||||
<p class="text-h5 font-weight-bold">
|
||||
{{ $t('paymentConfirmation.failure.thanks') }}
|
||||
{{ creatorUserName }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Back Button -->
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="text-white px-5 py-3"
|
||||
@click="router.push({ path: `/@${creatorUserName}` })"
|
||||
>
|
||||
{{ $t('paymentConfirmation.failure.return') }}
|
||||
{{ creatorUserName }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const client = useClient();
|
||||
|
||||
const creatorId = route.params.creatorId;
|
||||
const creatorUserName = ref('');
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
const response = await client.get(`/api/creators/${creatorId}`);
|
||||
creatorUserName.value = response.data.name;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch creator data:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-container {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
92
frontend/src/views/StripePayment.vue
Normal file
92
frontend/src/views/StripePayment.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<v-container>
|
||||
|
||||
<v-row>
|
||||
<v-text-field label="Message (facultatif)" v-model="tipMessage"
|
||||
style="border-radius: 10px; margin-top: 10px; margin-bottom: 10px; color: #a30e79; background-color: #f4f4f4">
|
||||
</v-text-field>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-text-field label="Montant ($)" v-model="price"
|
||||
style="border-radius: 10px; margin-bottom: 10px; color: #a30e79; background-color: #f4f4f4">
|
||||
</v-text-field>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="center">
|
||||
<v-btn @click="goPay()"
|
||||
style="margin-bottom: 10px; width: 200px; background-color: #6b0065; color: white; font-weight: bold;">
|
||||
<v-icon left style="margin-right: 10px;">
|
||||
mdi-gift
|
||||
</v-icon>
|
||||
Envoyez
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<v-dialog v-model="isPaymentDialogActive" max-width="720" persistent>
|
||||
<template v-slot:default>
|
||||
<v-card>
|
||||
<div id="checkout">
|
||||
<!-- Checkout will insert the payment form here -->
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn block class="ma-auto" style="width: 200px;" text="Annuler" @click="closeDialog()"></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const props = defineProps(['creatorId'])
|
||||
|
||||
let stripe = null;
|
||||
const client = useClient();
|
||||
const price = ref(0);
|
||||
const tipMessage = ref("");
|
||||
const isPaymentDialogActive = ref(false);
|
||||
var checkout;
|
||||
|
||||
onMounted(async () => {
|
||||
stripe = await loadStripe(import.meta.env.VITE_STRIPE_API_KEY);
|
||||
})
|
||||
|
||||
const fetchClientSecret = async () => {
|
||||
const clientSecret = await createCheckoutSession();
|
||||
return clientSecret;
|
||||
};
|
||||
|
||||
async function createCheckoutSession() {
|
||||
let clientSecret = await client.post('/api/Stripe', {
|
||||
amount: (price.value * 100),
|
||||
tipMessage: tipMessage.value,
|
||||
creatorId: props.creatorId
|
||||
});
|
||||
|
||||
let secret = clientSecret["data"];
|
||||
return secret;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
isPaymentDialogActive.value = false;
|
||||
checkout.destroy();
|
||||
}
|
||||
|
||||
async function goPay() {
|
||||
isPaymentDialogActive.value = true;
|
||||
|
||||
checkout = await stripe.initEmbeddedCheckout({
|
||||
fetchClientSecret,
|
||||
});
|
||||
|
||||
await checkout.mount('#checkout');
|
||||
}
|
||||
|
||||
</script>
|
||||
21
frontend/src/views/browser/CreatorCard.vue
Normal file
21
frontend/src/views/browser/CreatorCard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
32
frontend/src/views/browser/CreatorList.vue
Normal file
32
frontend/src/views/browser/CreatorList.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
250
frontend/src/views/contents/ContentCard.vue
Normal file
250
frontend/src/views/contents/ContentCard.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<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>
|
||||
115
frontend/src/views/contents/ContentEditorPage.vue
Normal file
115
frontend/src/views/contents/ContentEditorPage.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<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" :style="{ backgroundColor: creatorData.colors?.background, color: creatorData?.colors?.onSurface}">
|
||||
<div class="max-w-[1000px] mx-auto shadow-2xl rounded-lg overflow-hidden">
|
||||
<header class="text-2xl text-center py-4" :style="{ backgroundColor: creatorData?.colors?.primary, color: creatorData?.colors?.onPrimary }">
|
||||
Éditeur de contenu
|
||||
</header>
|
||||
|
||||
<div class="flex flex-grow">
|
||||
<aside class="side-menu flex flex-col items-center py-6 " :style="{ backgroundColor: creatorData?.colors?.secondary, color: creatorData?.colors?.onSecondary }">
|
||||
<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>
|
||||
124
frontend/src/views/contents/ContentList.vue
Normal file
124
frontend/src/views/contents/ContentList.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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" :style="{color:branding.colors.onSurface}">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>
|
||||
13
frontend/src/views/contents/ContentPage.vue
Normal file
13
frontend/src/views/contents/ContentPage.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
194
frontend/src/views/contents/HTMLContentEditor.vue
Normal file
194
frontend/src/views/contents/HTMLContentEditor.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<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, value, meta) => {
|
||||
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',
|
||||
items: [
|
||||
{
|
||||
type: 'colorpicker',
|
||||
name: 'colorpicker',
|
||||
label: 'Background Color'
|
||||
}
|
||||
]
|
||||
},
|
||||
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>
|
||||
31
frontend/src/views/contents/ImageViewer.vue
Normal file
31
frontend/src/views/contents/ImageViewer.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<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>
|
||||
105
frontend/src/views/contents/PostContent.vue
Normal file
105
frontend/src/views/contents/PostContent.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<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>
|
||||
148
frontend/src/views/contents/PostContentMenu.vue
Normal file
148
frontend/src/views/contents/PostContentMenu.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<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>
|
||||
157
frontend/src/views/contents/PublishContentButton.vue
Normal file
157
frontend/src/views/contents/PublishContentButton.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<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"
|
||||
:style="{
|
||||
border: `3px solid ${brandingStore.value.colors.menu}`
|
||||
}">
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
304
frontend/src/views/contents/QuickyContentEditor.vue
Normal file
304
frontend/src/views/contents/QuickyContentEditor.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<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>
|
||||
332
frontend/src/views/contents/Reaction.vue
Normal file
332
frontend/src/views/contents/Reaction.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<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>
|
||||
26
frontend/src/views/contents/YoutubePlayer.vue
Normal file
26
frontend/src/views/contents/YoutubePlayer.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
121
frontend/src/views/contents/contentcards/NContentCard.vue
Normal file
121
frontend/src/views/contents/contentcards/NContentCard.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<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]"
|
||||
:style="{
|
||||
backgroundColor: branding.colors.surface,
|
||||
boxShadow: '0 10px 10px rgba(0, 0, 0, 0.3)',
|
||||
borderColor: `rgba(${hexToRgb(branding.colors.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" :style="{color:branding.colors.onSurface}">{{ 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 :style="{color:branding.colors.onSurface}">
|
||||
<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" :style="{color:branding.colors.onSurface}">{{ 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>
|
||||
|
||||
246
frontend/src/views/contents/contentcards/SmContentCard.vue
Normal file
246
frontend/src/views/contents/contentcards/SmContentCard.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<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>
|
||||
@@ -0,0 +1,212 @@
|
||||
<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>
|
||||
@@ -0,0 +1,164 @@
|
||||
<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>
|
||||
285
frontend/src/views/creators/BannerActions.vue
Normal file
285
frontend/src/views/creators/BannerActions.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||
import DonationButtonBanner from '@/views/creators/DonationButtonBanner.vue';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import IconAccountVerified from "@/components/icons/IconAccountVerified.vue";
|
||||
|
||||
const brandingStore = useBrandingStore();
|
||||
const isMobile = ref(false);
|
||||
const creator = ref(null);
|
||||
const baseURL = window.location.origin;
|
||||
const creatorName = window.location.pathname.split('/@').pop();
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.innerWidth <= 640;
|
||||
}
|
||||
|
||||
// Récupération des URLs des réseaux sociaux
|
||||
function GetSocialsUrls() {
|
||||
const socials = [];
|
||||
const brandingSocials = brandingStore.value.socials;
|
||||
|
||||
if (brandingSocials.facebookUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-facebook',
|
||||
url: brandingSocials.facebookUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.instagramUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-instagram',
|
||||
url: brandingSocials.instagramUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.xUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-twitter',
|
||||
url: brandingSocials.xUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.linkedInUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-linkedin',
|
||||
url: brandingSocials.linkedInUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.tikTokUrl) {
|
||||
socials.push({
|
||||
icon: '/images/socials/tiktok-white.png',
|
||||
url: brandingSocials.tikTokUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.youtubeUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-youtube',
|
||||
url: brandingSocials.youtubeUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.redditUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-reddit',
|
||||
url: brandingSocials.redditUrl,
|
||||
});
|
||||
}
|
||||
if (brandingSocials.websiteUrl) {
|
||||
socials.push({
|
||||
icon: 'mdi-web',
|
||||
url: brandingSocials.websiteUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return socials;
|
||||
}
|
||||
|
||||
const isSticky = ref(false);
|
||||
const mainContainer = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
updateIsMobile();
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isSticky.value = !entry.isIntersecting;
|
||||
},
|
||||
{ threshold: 0 }
|
||||
);
|
||||
|
||||
if (mainContainer.value) {
|
||||
observer.observe(mainContainer.value);
|
||||
}
|
||||
|
||||
const client = useClient();
|
||||
|
||||
try {
|
||||
const creatorResponse = await client.get(`/api/creators/@${creatorName}`);
|
||||
creator.value = creatorResponse.data;
|
||||
} catch (error) {
|
||||
creator.value = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column w-full">
|
||||
<!-- Container principal avec le profil -->
|
||||
<div class="relative w-full shadow-xl rounded-2xl">
|
||||
<div
|
||||
ref="mainContainer"
|
||||
class="rounded-b-2xl shadow-2xl"
|
||||
:style="{
|
||||
backgroundColor: brandingStore.colors.primary,
|
||||
boxShadow: '0 5px 10px rgba(0, 0, 0, 0.3)',
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<!-- Profile et Info -->
|
||||
<div>
|
||||
<!-- Version PC -->
|
||||
<div v-show="!isMobile" class="items-start">
|
||||
<div>
|
||||
<img
|
||||
class="shadow-2xl rounded-full border-solid border-102 absolute z-20 max-w-[190px] ml-10 -mt-5"
|
||||
:src="
|
||||
brandingStore.value.images.logo
|
||||
? brandingStore.value.images.logo
|
||||
: '/images/placeholders/logo.png'
|
||||
"
|
||||
alt="Profile Picture"
|
||||
:style="{
|
||||
borderColor: brandingStore.colors.secondary,
|
||||
height: '190px',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ml-64 w-25 min-w-60 flex flex-row"
|
||||
:style="{ color: brandingStore.colors.onPrimary }"
|
||||
>
|
||||
<div v-show="brandingStore.value.verified" class="text-blue m-4 align-content-center verifiedhook">
|
||||
<icon-account-verified></icon-account-verified>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<span class="capitalize text-3xl titlepos">
|
||||
{{ brandingStore.value.name }}
|
||||
</span>
|
||||
<span class="capitalize text-lg titlepos">
|
||||
{{ brandingStore.value.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Mobile -->
|
||||
<div class="relative">
|
||||
<div
|
||||
:style="{
|
||||
borderColor: brandingStore.colors.secondary,
|
||||
height: '80px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-show="isMobile"
|
||||
class="absolute -top-7 left-0 px-3 flex flex-row items-center z-30"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
class="shadow-2xl rounded-full border-solid z-20 max-w-[150px]"
|
||||
:src="
|
||||
brandingStore.value.images.logo
|
||||
? brandingStore.value.images.logo
|
||||
: '/images/placeholders/logo.png'
|
||||
"
|
||||
alt="Profile Picture"
|
||||
:style="{ height: '135px' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="brandingStore.value.verified" class="text-blue m-4 align-content-center">
|
||||
<icon-account-verified></icon-account-verified>
|
||||
</div>
|
||||
<div class="ml-3 text-white w-full flex flex-col items-start">
|
||||
<p class="capitalize text-2xl">
|
||||
{{ brandingStore.value.name }}
|
||||
</p>
|
||||
<p class="capitalize text-md">
|
||||
{{ brandingStore.value.title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions - Follow et Register -->
|
||||
<!-- <div class="flex flex-col items-center justify-center w-full">-->
|
||||
<!-- <div class="flex flex-row space-x-1 justify-center mt-3 mb-2">-->
|
||||
<!-- <!–<subscribe-button></subscribe-button>–>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<!-- Bouton Support -->
|
||||
<div
|
||||
v-show="brandingStore.value.acceptDonation"
|
||||
class="z-20 shadow-2xl rounded-md text-white flex justify-center items-center z-50"
|
||||
:class="{
|
||||
'absolute bottom-6 right-8 w-64 h-28 ': !isMobile,
|
||||
'fixed bottom-0 left-0 right-0 w-full h-16': isMobile,
|
||||
}"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary }"
|
||||
>
|
||||
<donation-button-banner
|
||||
v-if="creator"
|
||||
:creator-id="creator.id"
|
||||
:creator-name="creator.name"
|
||||
:on-success-url="baseURL + '/paymentcompleted/' + creator.id"
|
||||
:on-cancelled-url="baseURL + '/paymentfailed/' + creator.id"
|
||||
></donation-button-banner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section pour les icônes de réseaux sociaux -->
|
||||
<div
|
||||
class="rounded-b-2xl -mt-3 h-12 px-36 flex flex-col items-center justify-center"
|
||||
:style="{
|
||||
backgroundColor: brandingStore.colors.secondary,
|
||||
boxShadow: '0 5px 20px rgba(0, 0, 0, 0.3)',
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-evenly mt-3 w-full">
|
||||
<div class="flex flex-row space-x-6 justify-center">
|
||||
<a
|
||||
v-for="socialNetwork in GetSocialsUrls()"
|
||||
:key="socialNetwork.url"
|
||||
:href="socialNetwork.url"
|
||||
target="_blank"
|
||||
class="text-white text-md transform transition-transform duration-200 hover:scale-125 hover:text-blue-500"
|
||||
>
|
||||
<v-icon v-if="socialNetwork.icon.includes('mdi')">
|
||||
{{ socialNetwork.icon }}
|
||||
</v-icon>
|
||||
<img
|
||||
v-else
|
||||
:src="socialNetwork.icon"
|
||||
class="w-6 h-6 mt-0.5"
|
||||
:alt="socialNetwork.url"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-button {
|
||||
@apply rounded flex justify-center font-sans py-1 text-white tracking-widest p-4;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
@apply bg-purple-800;
|
||||
}
|
||||
|
||||
/* Transition CSS */
|
||||
.transition-all {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.titlepos {
|
||||
position: relative;
|
||||
top: 30px;
|
||||
}
|
||||
.verifiedhook{
|
||||
position: relative;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
</style>
|
||||
94
frontend/src/views/creators/CreatorBanner.vue
Normal file
94
frontend/src/views/creators/CreatorBanner.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- PC -->
|
||||
<div v-if="!isMobile">
|
||||
<div class="shadow-lg rounded-2xl mt-2">
|
||||
<div class="relative z-20">
|
||||
<div class="min-h-8 rounded-t-2xl shadow-lg" :style="{ backgroundColor: branding.colors.primary }"></div>
|
||||
<!-- Banner -->
|
||||
<div class="relative">
|
||||
<div>
|
||||
<img
|
||||
class="w-full drop-shadow-[0_10px_6px_rgba(0,0,0,0.25)]"
|
||||
:src="branding.value.images.banner ? branding.value.images.banner : '/images/placeholders/banner.png'"
|
||||
alt="Profile Banner"
|
||||
style="max-height: 425px"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<banner-actions></banner-actions>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile -->
|
||||
<div v-if="isMobile">
|
||||
<div class="shadow-lg rounded-2xl ">
|
||||
<div class="relative z-20">
|
||||
<div class="shadow-2xl flex items-center px-2 py-2"
|
||||
:style="{ backgroundColor: branding.colors.primary, color: branding.colors.onPrimary }">
|
||||
|
||||
<router-link to="/@Hutopy">
|
||||
<div class="flex items-center">
|
||||
<HutopySvg></HutopySvg>
|
||||
<div class="text-xl font-bold -ml-2 ">Hutopy</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<router-link to="/login">
|
||||
<button class="lg:hidden flex items-center justify-center mr-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
:stroke="branding.colors.onPrimary" class="w-8 h-8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Banner -->
|
||||
<div class="relative">
|
||||
<div>
|
||||
<img
|
||||
class="w-full drop-shadow-[0_10px_6px_rgba(0,0,0,0.25)]"
|
||||
:src="branding.value.images.banner ? branding.value.images.banner : '/images/placeholders/banner.png'"
|
||||
alt="Profile Banner"
|
||||
style="max-height: 425px"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<banner-actions></banner-actions>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount} from "vue";
|
||||
import BannerActions from "@/views/creators/BannerActions.vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import HutopySvg from "@/views/svg/HutopySvg.vue";
|
||||
|
||||
|
||||
const branding = useBrandingStore();
|
||||
const isMobile = ref(false);
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.innerWidth <= 640;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile();
|
||||
window.addEventListener("resize", updateIsMobile);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", updateIsMobile);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
28
frontend/src/views/creators/CreatorContent.vue
Normal file
28
frontend/src/views/creators/CreatorContent.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
634
frontend/src/views/creators/CreatorHome.vue
Normal file
634
frontend/src/views/creators/CreatorHome.vue
Normal file
@@ -0,0 +1,634 @@
|
||||
<template>
|
||||
<div v-if="creatorProfileStore.creator.id === brandingStore.value.id" class="flex justify-end space-x-2 mb-5 pa-1">
|
||||
<!-- Bouton principal : Éditer ou Enregistrer -->
|
||||
<button
|
||||
v-if="isLoggedIn"
|
||||
@click="isEditMode ? saveChanges() : toggleEditMode()"
|
||||
class="px-4 py-2 rounded-md hover:opacity-90"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"
|
||||
>
|
||||
{{ isEditMode ? 'Enregistrer' : 'Éditer' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="isEditMode && isLoggedIn"
|
||||
@click="cancelEdit"
|
||||
class="px-4 py-2 rounded-md hover:opacity-90 bg-red-500 text-white"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-8 px-6 rounded-2xl py-8 shadow-2xl"
|
||||
:style="{ backgroundColor: brandingStore.colors.primary, color: brandingStore.colors.onPrimary }">
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Titre principal -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2"> Titre</div>
|
||||
<textarea v-model="editableMainTitle" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<h1 v-else-if="mainTitle" class="text-4xl font-bold text-center ">{{ mainTitle }}</h1>
|
||||
|
||||
<!-- Image principale -->
|
||||
<div class="relative flex justify-center">
|
||||
<label v-if="isEditMode">
|
||||
<input type="file" @change="updateImage('mainImageUrl', $event)" class="hidden"/>
|
||||
<img :src="mainImageUrl || fallbackImage"
|
||||
alt="Image principale"
|
||||
class="rounded-md max-w-full h-auto cursor-pointer"/>
|
||||
</label>
|
||||
<img v-else-if="mainImageUrl" :src="mainImageUrl"
|
||||
alt="Image principale"
|
||||
class="rounded-md max-w-full h-auto cursor-pointer"
|
||||
@click="openFullscreen(mainImageUrl)"/>
|
||||
|
||||
<button v-if="isEditMode" @click="deleteImage('mainImageUrl')"
|
||||
class="absolute top-10 right-2 px-2 py-1 bg-red-500 text-white hover:bg-red-600">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Texte sous l'image principale -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2"> Description</div>
|
||||
<textarea v-model="editableMainImageText" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<p v-else-if="mainImageText" class="text-lg text-justify">
|
||||
{{ mainImageText }}
|
||||
</p>
|
||||
|
||||
<!-- Titre video principale -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2"> Titre Vidéo Princpiale</div>
|
||||
<textarea v-model="editableVideoSubtitleMain" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<h2 v-else-if="videoSubtitleMain" class="text-2xl font-semibold text-center" >
|
||||
{{ videoSubtitleMain }}
|
||||
</h2>
|
||||
|
||||
<!-- Vidéo YouTube principale -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2">URL vidéo</div>
|
||||
<input
|
||||
v-if="isEditMode"
|
||||
type="text"
|
||||
v-model="editableVideoUrlMain"
|
||||
class="w-full p-2 border rounded-md"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<div v-if="isEditMode"></div>
|
||||
<div v-else-if="videoUrlMain" class="video-container">
|
||||
<iframe
|
||||
:src="videoUrlMain"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="video-frame">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Texte sous video principale -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2"> Description</div>
|
||||
<textarea v-model="editableMainVideoText" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<p v-else-if="mainVideoText" class="text-lg text-justify">
|
||||
{{ mainVideoText }}
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Sous-titre avant les deux images -->
|
||||
<div v-if="isEditMode">
|
||||
<div v-if="isEditMode" class="text-2xl py-2"> Sous-titre</div>
|
||||
<textarea v-model="editableImagesSubtitle" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<h2 v-else-if="imagesSubtitle" class="text-2xl font-semibold text-center">
|
||||
{{ imagesSubtitle }}
|
||||
</h2>
|
||||
|
||||
<!-- 4 images côte à côte -->
|
||||
<FullscreenImage ref="fullscreenImage" :image-url="currentImage" />
|
||||
<div>
|
||||
<!-- Mode édition -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2">Images</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<!-- Première image -->
|
||||
<div class="relative">
|
||||
<label>
|
||||
<input type="file" @change="updateImage('image1Url', $event)" class="hidden" />
|
||||
<img :src="image1Url || fallbackImage"
|
||||
alt="Image 1"
|
||||
class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</label>
|
||||
<button @click="deleteImage('image1Url')"
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-red-500 text-white hover:bg-red-600">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Deuxième image -->
|
||||
<div class="relative">
|
||||
<label>
|
||||
<input type="file" @change="updateImage('image2Url', $event)" class="hidden" />
|
||||
<img :src="image2Url || fallbackImage"
|
||||
alt="Image 2"
|
||||
class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</label>
|
||||
<button @click="deleteImage('image2Url')"
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-red-500 text-white hover:bg-red-600">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Troisième image -->
|
||||
<div class="relative">
|
||||
<label>
|
||||
<input type="file" @change="updateImage('image3Url', $event)" class="hidden" />
|
||||
<img :src="image3Url || fallbackImage"
|
||||
alt="Image 3"
|
||||
class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</label>
|
||||
<button @click="deleteImage('image3Url')"
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-red-500 text-white hover:bg-red-600">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Quatrième image -->
|
||||
<div class="relative">
|
||||
<label>
|
||||
<input type="file" @change="updateImage('image4Url', $event)" class="hidden" />
|
||||
<img :src="image4Url || fallbackImage"
|
||||
alt="Image 4"
|
||||
class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</label>
|
||||
<button @click="deleteImage('image4Url')"
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-red-500 text-white hover:bg-red-600">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode normal -->
|
||||
<div v-else>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<!-- Première image -->
|
||||
<div class="relative w-full sm:flex-1 sm:min-w-[calc(25%-1rem)]" v-if="image1Url" @click="openFullscreen(image1Url)">
|
||||
<img :src="image1Url" alt="Image 1" class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<!-- Deuxième image -->
|
||||
<div class="relative w-full sm:flex-1 sm:min-w-[calc(25%-1rem)]" v-if="image2Url" @click="openFullscreen(image2Url)">
|
||||
<img :src="image2Url" alt="Image 2" class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<!-- Troisième image -->
|
||||
<div class="relative w-full sm:flex-1 sm:min-w-[calc(25%-1rem)]" v-if="image3Url" @click="openFullscreen(image3Url)">
|
||||
<img :src="image3Url" alt="Image 3" class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<!-- Quatrième image -->
|
||||
<div class="relative w-full sm:flex-1 sm:min-w-[calc(25%-1rem)]" v-if="image4Url" @click="openFullscreen(image4Url)">
|
||||
<img :src="image4Url" alt="Image 4" class="rounded-md max-w-full h-auto cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Texte sous les deux images -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2"> Images</div>
|
||||
<textarea v-model="editableImagesText" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<p v-else-if="imagesText" class="text-lg text-justify">
|
||||
{{ imagesText }}
|
||||
</p>
|
||||
|
||||
<!-- Sous-titre avant la vidéo -->
|
||||
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2"> Titre Video</div>
|
||||
<textarea v-model="editableVideoSubtitle" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<h2 v-else-if="videoSubtitle" class="text-2xl font-semibold text-center">
|
||||
{{ videoSubtitle }}
|
||||
</h2>
|
||||
|
||||
<!-- Vidéo YouTube -->
|
||||
<div v-if="isEditMode">
|
||||
<div class="text-2xl py-2">URL vidéo</div>
|
||||
<input
|
||||
v-if="isEditMode"
|
||||
type="text"
|
||||
v-model="editableVideoUrl"
|
||||
class="w-full p-2 border rounded-md"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<div v-if="isEditMode"></div>
|
||||
<iframe
|
||||
v-else-if="videoUrl"
|
||||
:src="videoUrl"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="rounded-md"
|
||||
style="width: 600px; height: 337px;"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Texte sous la vidéo -->
|
||||
<div v-if="isEditMode" class="text-2xl"> Description</div>
|
||||
<div v-if="isEditMode">
|
||||
<textarea v-model="editableVideoText" class="w-full p-2 border rounded-md h-24"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"></textarea>
|
||||
</div>
|
||||
<p v-else-if="videoText" class="text-lg text-justify">
|
||||
{{ videoText }}
|
||||
</p>
|
||||
|
||||
<!-- Informations de contact -->
|
||||
<div class="flex flex-col space-y-6 mt-8">
|
||||
<div v-if="isEditMode" class="flex flex-col space-y-2">
|
||||
<!-- Édition du téléphone -->
|
||||
<div>
|
||||
<label class="text-lg">Numéro de téléphone</label>
|
||||
<input
|
||||
v-model="editablePhoneNumber"
|
||||
type="text"
|
||||
class="w-full p-2 border rounded-md"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"
|
||||
/>
|
||||
</div>
|
||||
<!-- Édition de l'email -->
|
||||
<div>
|
||||
<label class="text-lg">Adresse email</label>
|
||||
<input
|
||||
v-model="editableEmail"
|
||||
type="text"
|
||||
class="w-full p-2 border rounded-md"
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col sm:flex-row sm:space-x-64 space-y-4 sm:space-y-0 justify-center items-center"
|
||||
>
|
||||
<!-- Affichage du téléphone -->
|
||||
<div v-if="editablePhoneNumber" class="flex items-center space-x-2">
|
||||
<i class="mdi mdi-phone-outline text-2xl"></i>
|
||||
<span>{{ editablePhoneNumber }}</span>
|
||||
</div>
|
||||
<!-- Affichage de l'email -->
|
||||
<div v-if="editableEmail" class="flex items-center space-x-2">
|
||||
<i class="mdi mdi-email-outline text-2xl"></i>
|
||||
<a
|
||||
:href="`mailto:${editableEmail}`"
|
||||
class="no-underline text-current"
|
||||
>
|
||||
{{ editableEmail }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from "vue";
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { watch} from "vue";
|
||||
import FullscreenImage from "@/views/creators/FullscreenImage.vue";
|
||||
|
||||
const { smAndDown } = useDisplay();
|
||||
const isMobileView = ref(smAndDown.value);
|
||||
|
||||
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const brandingStore = useBrandingStore();
|
||||
const client = useClient();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const isLoggedIn = true;
|
||||
const isEditMode = ref(false);
|
||||
|
||||
|
||||
|
||||
const currentImage = ref("");
|
||||
const fullscreenImage = ref(null);
|
||||
|
||||
function openFullscreen(imageUrl) {
|
||||
currentImage.value = imageUrl;
|
||||
fullscreenImage.value.open();
|
||||
}
|
||||
|
||||
watch(smAndDown, (newVal) => {
|
||||
isMobileView.value = newVal;
|
||||
});
|
||||
|
||||
// Image de fallback pour l'éditeur
|
||||
const fallbackImage = "https://via.placeholder.com/300?text=Image+non+disponible";
|
||||
|
||||
// Variables réactives pour les données
|
||||
const editablePhoneNumber = ref("");
|
||||
const editableEmail = ref("");
|
||||
const mainTitle = ref("");
|
||||
const mainImageUrl = ref("");
|
||||
const mainImageText = ref("");
|
||||
const mainVideoText = ref("");
|
||||
const imagesSubtitle = ref("");
|
||||
const image1Url = ref("");
|
||||
const image2Url = ref("");
|
||||
const image3Url = ref("");
|
||||
const image4Url = ref("");
|
||||
const imagesText = ref("");
|
||||
const videoSubtitle = ref("");
|
||||
const videoSubtitleMain = ref("");
|
||||
const videoUrlMain = ref("");
|
||||
const videoUrl = ref("");
|
||||
const videoText = ref("");
|
||||
const phoneNumber = ref("");
|
||||
const email = ref("");
|
||||
|
||||
|
||||
const editableImages = ref([null, null, null, null]);
|
||||
|
||||
|
||||
// Editable fields
|
||||
const editableMainTitle = ref("");
|
||||
const editableMainImageText = ref("");
|
||||
const editableMainVideoText = ref("");
|
||||
const editableImagesSubtitle = ref("");
|
||||
const editableImagesText = ref("");
|
||||
const editableVideoSubtitle = ref("");
|
||||
const editableVideoSubtitleMain = ref("");
|
||||
const editableVideoText = ref("");
|
||||
const editableVideoUrlMain = ref("");
|
||||
const editableVideoUrl = ref("");
|
||||
|
||||
// Activer/désactiver le mode édition
|
||||
function toggleEditMode() {
|
||||
isEditMode.value = !isEditMode.value;
|
||||
if (isEditMode.value) {
|
||||
// Charger les valeurs pour l'édition
|
||||
editableMainTitle.value = mainTitle.value;
|
||||
editableMainImageText.value = mainImageText.value;
|
||||
editableMainVideoText.value = mainVideoText.value;
|
||||
editableImagesSubtitle.value = imagesSubtitle.value;
|
||||
editableImagesText.value = imagesText.value;
|
||||
editableVideoSubtitle.value = videoSubtitle.value;
|
||||
editableVideoSubtitleMain.value = videoSubtitleMain.value;
|
||||
editableVideoText.value = videoText.value;
|
||||
editableVideoUrlMain.value = videoUrlMain.value;
|
||||
editableVideoUrl.value = videoUrl.value;
|
||||
editablePhoneNumber.value = phoneNumber.value;
|
||||
editableEmail.value = email.value;
|
||||
} else {
|
||||
// Sauvegarder les modifications ou réinitialiser les URLs des images supprimées
|
||||
mainTitle.value = editableMainTitle.value;
|
||||
mainImageText.value = editableMainImageText.value;
|
||||
mainVideoText.value = editableMainVideoText.value;
|
||||
imagesSubtitle.value = editableImagesSubtitle.value;
|
||||
imagesText.value = editableImagesText.value;
|
||||
videoSubtitle.value = editableVideoSubtitle.value;
|
||||
videoSubtitleMain.value = editableVideoSubtitleMain.value;
|
||||
videoText.value = editableVideoText.value;
|
||||
videoUrlMain.value = editableVideoUrlMain.value;
|
||||
videoUrl.value = editableVideoUrl.value;
|
||||
phoneNumber.value = editablePhoneNumber.value;
|
||||
email.value = editableEmail.value;
|
||||
|
||||
// Réinitialisation des images supprimées à des strings vides si nécessaire
|
||||
if (mainImageUrl.value === null) mainImageUrl.value = "";
|
||||
if (image1Url.value === null) image1Url.value = "";
|
||||
if (image2Url.value === null) image2Url.value = "";
|
||||
if (image3Url.value === null) image3Url.value = "";
|
||||
if (image4Url.value === null) image4Url.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Supprimer une image
|
||||
function deleteImage(field) {
|
||||
switch (field) {
|
||||
case "mainImageUrl":
|
||||
mainImageUrl.value = ""; // Remplace par un string vide
|
||||
break;
|
||||
case "image1Url":
|
||||
image1Url.value = ""; // Remplace par un string vide
|
||||
break;
|
||||
case "image2Url":
|
||||
image2Url.value = ""; // Remplace par un string vide
|
||||
break;
|
||||
case "image3Url":
|
||||
image3Url.value = ""; // Remplace par un string vide
|
||||
break;
|
||||
case "image4Url":
|
||||
image4Url.value = ""; // Remplace par un string vide
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Mettre à jour une image
|
||||
function updateImage(field, event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
// Stocker le fichier dans editableImages pour l'envoi
|
||||
switch (field) {
|
||||
case "mainImageUrl":
|
||||
editableImages.value[0] = file;
|
||||
mainImageUrl.value = URL.createObjectURL(file);
|
||||
break;
|
||||
case "image1Url":
|
||||
editableImages.value[1] = file;
|
||||
image1Url.value = URL.createObjectURL(file);
|
||||
break;
|
||||
case "image2Url":
|
||||
editableImages.value[2] = file;
|
||||
image2Url.value = URL.createObjectURL(file);
|
||||
break;
|
||||
case "image3Url":
|
||||
editableImages.value[3] = file;
|
||||
image3Url.value = URL.createObjectURL(file);
|
||||
break;
|
||||
case "image4Url":
|
||||
editableImages.value[4] = file;
|
||||
image4Url.value = URL.createObjectURL(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Charger les données au montage
|
||||
onMounted(() => {
|
||||
mainTitle.value = brandingStore.presentationInfos.title;
|
||||
mainImageUrl.value = brandingStore.presentationInfos.mainImageUrl;
|
||||
mainImageText.value = brandingStore.presentationInfos.mainImageText;
|
||||
mainVideoText.value = brandingStore.presentationInfos.mainVideoText;
|
||||
imagesSubtitle.value = brandingStore.presentationInfos.imagesSubtitle;
|
||||
image1Url.value = brandingStore.presentationInfos.image1Url;
|
||||
image2Url.value = brandingStore.presentationInfos.image2Url;
|
||||
image3Url.value = brandingStore.presentationInfos.image3Url;
|
||||
image4Url.value = brandingStore.presentationInfos.image4Url;
|
||||
imagesText.value = brandingStore.presentationInfos.imagesText;
|
||||
videoSubtitle.value = brandingStore.presentationInfos.videoSubtitle;
|
||||
videoSubtitleMain.value = brandingStore.presentationInfos.videoSubtitleMain;
|
||||
videoUrl.value = brandingStore.presentationInfos.videoUrl;
|
||||
videoUrlMain.value = brandingStore.presentationInfos.videoUrlMain;
|
||||
videoText.value = brandingStore.presentationInfos.videoText;
|
||||
editablePhoneNumber.value = brandingStore.presentationInfos.phoneNumber;
|
||||
editableEmail.value= brandingStore.presentationInfos.email;
|
||||
phoneNumber.value = brandingStore.presentationInfos.phoneNumber;
|
||||
email.value = brandingStore.presentationInfos.email;
|
||||
});
|
||||
|
||||
async function saveChanges() {
|
||||
if (!creatorProfileStore.creator.id) {
|
||||
console.error("L'ID du créateur est manquant !");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Ajout des champs textuels
|
||||
formData.append("PhoneNumber", editablePhoneNumber.value || "");
|
||||
formData.append("Email", editableEmail.value || "");
|
||||
formData.append("Title", editableMainTitle.value || "");
|
||||
formData.append("MainImageText", editableMainImageText.value || "");
|
||||
formData.append("MainVideoText", editableMainVideoText.value || "");
|
||||
formData.append("ImagesSubtitle", editableImagesSubtitle.value || "");
|
||||
formData.append("ImagesText", editableImagesText.value || "");
|
||||
formData.append("VideoSubtitle", editableVideoSubtitle.value || "");
|
||||
formData.append("VideoSubtitleMain", editableVideoSubtitleMain.value || "");
|
||||
formData.append("VideoUrlMain", editableVideoUrlMain.value || "");
|
||||
formData.append("VideoUrl", editableVideoUrl.value || "");
|
||||
formData.append("VideoText", editableVideoText.value || "");
|
||||
|
||||
// Ajout des URLs d'images supprimées
|
||||
formData.append("MainImageUrl", mainImageUrl.value || ""); // Peut contenir un string vide
|
||||
formData.append("Image1Url", image1Url.value || "");
|
||||
formData.append("Image2Url", image2Url.value || "");
|
||||
formData.append("Image3Url", image3Url.value || "");
|
||||
formData.append("Image4Url", image4Url.value || "");
|
||||
|
||||
// Ajout des fichiers d'images téléversées
|
||||
if (editableImages.value[0]) formData.append("MainImage", editableImages.value[0]);
|
||||
if (editableImages.value[1]) formData.append("Image1", editableImages.value[1]);
|
||||
if (editableImages.value[2]) formData.append("Image2", editableImages.value[2]);
|
||||
if (editableImages.value[3]) formData.append("Image3", editableImages.value[3]);
|
||||
if (editableImages.value[4]) formData.append("Image4", editableImages.value[4]);
|
||||
|
||||
try {
|
||||
// Désactiver le bouton de sauvegarde pour éviter les clics multiples
|
||||
isLoading.value = true;
|
||||
|
||||
// Envoyer les données au backend
|
||||
const response = await client.post(
|
||||
`/api/creators/${creatorProfileStore.creator.id}/presentation-infos`,
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
||||
);
|
||||
|
||||
// Mettre à jour les valeurs locales pour refléter les changements
|
||||
mainTitle.value = editableMainTitle.value;
|
||||
mainImageText.value = editableMainImageText.value;
|
||||
mainVideoText.value = editableMainVideoText.value;
|
||||
imagesSubtitle.value = editableImagesSubtitle.value;
|
||||
imagesText.value = editableImagesText.value;
|
||||
videoSubtitle.value = editableVideoSubtitle.value;
|
||||
videoSubtitleMain.value = editableVideoSubtitleMain.value;
|
||||
videoText.value = editableVideoText.value;
|
||||
videoUrlMain.value = editableVideoUrlMain.value;
|
||||
videoUrl.value = editableVideoUrl.value;
|
||||
phoneNumber.value = editablePhoneNumber.value;
|
||||
email.value = editableEmail.value;
|
||||
|
||||
console.log("Données sauvegardées :", response.data);
|
||||
|
||||
// Réinitialiser le mode édition
|
||||
isEditMode.value = false;
|
||||
|
||||
// Rafraîchir après une légère pause pour s'assurer des mises à jour visuelles
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
} finally {
|
||||
// Réactiver les interactions
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function cancelEdit() {
|
||||
// Restaurer les valeurs d'origine
|
||||
editableMainTitle.value = mainTitle.value;
|
||||
editableMainImageText.value = mainImageText.value;
|
||||
editableMainVideoText.value = mainVideoText.value;
|
||||
editableImagesSubtitle.value = imagesSubtitle.value;
|
||||
editableImagesText.value = imagesText.value;
|
||||
editableVideoSubtitle.value = videoSubtitle.value;
|
||||
editableVideoSubtitleMain.value = videoSubtitleMain.value;
|
||||
editableVideoText.value = videoText.value;
|
||||
editableVideoUrlMain.value = videoUrlMain.value;
|
||||
editableVideoUrl.value = videoUrl.value;
|
||||
editablePhoneNumber.value = phoneNumber.value;
|
||||
editableEmail.value = email.value;
|
||||
|
||||
// Désactiver le mode édition
|
||||
isEditMode.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 56.25%; /* Ratio 16:9 (9/16 = 0.5625) */
|
||||
}
|
||||
|
||||
.video-frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
border-radius: 0.5rem; /* Pour les bords arrondis */
|
||||
}
|
||||
</style>
|
||||
27
frontend/src/views/creators/CreatorLayout.vue
Normal file
27
frontend/src/views/creators/CreatorLayout.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
|
||||
<div class="flex flex-col min-h-screen max-w-[1100px] mx-auto">
|
||||
<div v-if="brandingStore.loading">
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</div>
|
||||
<div v-else>
|
||||
<creator-banner></creator-banner>
|
||||
</div>
|
||||
<div class="py-8 flex-grow">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script async setup>
|
||||
import CreatorBanner from "@/views/creators/CreatorBanner.vue";
|
||||
import Footer from "@/views/main/Footer.vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
const brandingStore = useBrandingStore()
|
||||
</script>
|
||||
165
frontend/src/views/creators/DonationButton.vue
Normal file
165
frontend/src/views/creators/DonationButton.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<v-btn class="hover:scale-125" variant="text" icon @click="openDonationDialog()">
|
||||
<v-icon :class="['text-2xl', iconColorClass]">mdi-gift-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
|
||||
<v-dialog v-model="donationModal" max-width="500">
|
||||
<v-form>
|
||||
<v-card class="text-center rounded-xl" :style="{ border: `3px solid ${brandingStore.colors.primary}` }">
|
||||
<div class="py-4 text-2xl font-bold border-b mb-2">
|
||||
Je Soutiens!
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center px-3">
|
||||
<img
|
||||
:src="brandingStore.value.images.logo"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="40"
|
||||
height="40"
|
||||
:style="{ border: `2px solid ${brandingStore.colors.secondary}` }">
|
||||
<div class="capitalize px-2 text-2xl">{{ brandingStore.value.name }}</div>
|
||||
<v-btn icon @click="closeDonationDialog()" class="ml-auto" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="tipAmountInDollars"
|
||||
type="number"
|
||||
:min="0"
|
||||
class="p-2"
|
||||
label="Montant"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
inputmode="numeric"
|
||||
@keydown="preventNonNumeric"
|
||||
prepend-inner-icon="mdi-currency-usd"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea v-model="tipMessage"
|
||||
label="Message (facultatif)"
|
||||
class="p-2"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
></v-textarea>
|
||||
|
||||
<v-btn variant="outlined"
|
||||
:style="{ borderColor: brandingStore.colors.primary, color: brandingStore.colors.primary }"
|
||||
@click="goPay()" class="w-full mt-5">
|
||||
Envoyez
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="isPaymentDialogActive" max-width="720" persistent>
|
||||
<template v-slot:default>
|
||||
<v-card>
|
||||
|
||||
<div id="checkout">
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn block class="ma-auto"
|
||||
style="width: 200px;"
|
||||
@click="closeDialog()">Annuler
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {loadStripe} from '@stripe/stripe-js';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
|
||||
const brandingStore = useBrandingStore()
|
||||
|
||||
const props = defineProps({
|
||||
creatorId: {default: 'missing-creator-id', required: true},
|
||||
creatorName: {default: 'missing-creator-name', required: true},
|
||||
onSuccessUrl: {default: 'missing-on-success-u', required: true},
|
||||
onCancelledUrl: {default: 'missing-on-cancelled-url', required: true},
|
||||
iconColorClass: {default: 'text-black'}
|
||||
});
|
||||
|
||||
const donationModal = ref(false);
|
||||
|
||||
function openDonationDialog() {
|
||||
donationModal.value = true
|
||||
}
|
||||
|
||||
function closeDonationDialog() {
|
||||
donationModal.value = false
|
||||
}
|
||||
|
||||
|
||||
const isPaymentDialogActive = ref(false);
|
||||
|
||||
const tipAmountInDollars = ref(0);
|
||||
const tipMessage = ref("");
|
||||
|
||||
let stripe = null;
|
||||
let checkout;
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
stripe = await loadStripe(import.meta.env.VITE_STRIPE_API_KEY);
|
||||
});
|
||||
|
||||
async function createCheckoutSession() {
|
||||
const client = useClient()
|
||||
|
||||
let clientSecret = await client.post(
|
||||
`/api/tips`,
|
||||
{
|
||||
amount: tipAmountInDollars.value * 100,
|
||||
currency: 'CAD',
|
||||
message: tipMessage.value,
|
||||
creatorId: props.creatorId,
|
||||
checkoutSuccessUrl: props.onSuccessUrl,
|
||||
checkoutCancelledUrl: props.onCancelledUrl
|
||||
});
|
||||
|
||||
return clientSecret.data;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
isPaymentDialogActive.value = false;
|
||||
if (checkout) {
|
||||
checkout.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async function goPay() {
|
||||
isPaymentDialogActive.value = true;
|
||||
|
||||
const response = await createCheckoutSession()
|
||||
|
||||
// Redirect to the Stripe Checkout page
|
||||
window.location.href = response.stripeCheckoutUrl
|
||||
}
|
||||
|
||||
function preventNonNumeric(event) {
|
||||
const key = event.key;
|
||||
const allowedKeys = ['Backspace', 'ArrowLeft', 'ArrowRight', 'Delete'];
|
||||
|
||||
if (!/^\d$/.test(key) && !allowedKeys.includes(key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
208
frontend/src/views/creators/DonationButtonBanner.vue
Normal file
208
frontend/src/views/creators/DonationButtonBanner.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<v-btn
|
||||
variant="text"
|
||||
style="font-size: x-large; height: 100%"
|
||||
block
|
||||
@click="openDonationDialog()"
|
||||
>
|
||||
{{ $t('isupportbtn.isupport') }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="donationModal" max-width="500">
|
||||
<v-form>
|
||||
<v-card
|
||||
class="text-center rounded-xl"
|
||||
:style="{ border: `3px solid ${brandingStore.colors.primary}` }"
|
||||
>
|
||||
<div class="py-4 text-2xl font-bold border-b mb-2"> {{ $t('isupportbtn.isupport') }}</div>
|
||||
|
||||
<div class="flex flex-row align-center px-3">
|
||||
<img
|
||||
:src="brandingStore.value.images.logo"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="40"
|
||||
height="40"
|
||||
:style="{ border: `2px solid ${brandingStore.colors.secondary}` }"
|
||||
/>
|
||||
<div class="capitalize px-2 text-2xl">
|
||||
{{ brandingStore.value.name }}
|
||||
</div>
|
||||
<v-btn
|
||||
icon
|
||||
@click="closeDonationDialog()"
|
||||
class="ml-auto"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="tipAmountInDollars"
|
||||
type="number"
|
||||
autofocus
|
||||
placeholder="0"
|
||||
:min="0"
|
||||
class="p-2"
|
||||
:label="`${$t('isupportbtn.amount')}`"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
inputmode="numeric"
|
||||
@keydown="preventNonNumeric"
|
||||
prepend-inner-icon="mdi-currency-usd"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="tipMessage"
|
||||
:label="`${$t('isupportbtn.message')}`"
|
||||
class="p-2"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
></v-textarea>
|
||||
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
:style="{
|
||||
borderColor: brandingStore.colors.primary,
|
||||
color: brandingStore.colors.primary,
|
||||
backgroundColor: brandingStore.colors.secondary,
|
||||
}"
|
||||
@click="goPay()"
|
||||
class="w-full mt-5"
|
||||
>
|
||||
{{ $t('isupportbtn.send') }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="isPaymentDialogActive" max-width="720" persistent>
|
||||
<template v-slot:default>
|
||||
<v-card :style="{ padding: '20px' }">
|
||||
<div id="checkout"></div>
|
||||
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
block
|
||||
class="ma-auto"
|
||||
style="width: 200px"
|
||||
@click="closeDialog()"
|
||||
>Annuler
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const brandingStore = useBrandingStore();
|
||||
|
||||
const props = defineProps({
|
||||
creatorId: { default: 'missing-creator-id', required: true },
|
||||
creatorName: { default: 'missing-creator-name', required: true },
|
||||
onSuccessUrl: { default: 'missing-on-success-u', required: true },
|
||||
onCancelledUrl: { default: 'missing-on-cancelled-url', required: true },
|
||||
iconColorClass: { default: 'text-black' },
|
||||
});
|
||||
|
||||
const errorMessage = ref('');
|
||||
|
||||
const donationModal = ref(false);
|
||||
|
||||
function openDonationDialog() {
|
||||
donationModal.value = true;
|
||||
}
|
||||
|
||||
function closeDonationDialog() {
|
||||
donationModal.value = false;
|
||||
}
|
||||
|
||||
const isPaymentDialogActive = ref(false);
|
||||
|
||||
const tipAmountInDollars = ref('');
|
||||
const tipMessage = ref('');
|
||||
|
||||
let stripe = null;
|
||||
let checkout;
|
||||
|
||||
onMounted(async () => {
|
||||
stripe = await loadStripe(import.meta.env.VITE_STRIPE_API_KEY);
|
||||
});
|
||||
|
||||
async function createCheckoutSession() {
|
||||
const client = useClient();
|
||||
try {
|
||||
let clientSecret = await client.post(`/api/tips`, {
|
||||
creatorId: props.creatorId,
|
||||
amount: tipAmountInDollars.value * 100,
|
||||
currency: 'CAD',
|
||||
message: tipMessage.value,
|
||||
checkoutSuccessUrl: props.onSuccessUrl,
|
||||
checkoutCancelledUrl: props.onCancelledUrl,
|
||||
});
|
||||
|
||||
return clientSecret.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorMessage.value = 'Une erreur est survenue. Veuillez réessayer.';
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
isPaymentDialogActive.value = false;
|
||||
errorMessage.value = '';
|
||||
|
||||
if (checkout) {
|
||||
checkout.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async function goPay() {
|
||||
isPaymentDialogActive.value = true;
|
||||
|
||||
const response = await createCheckoutSession();
|
||||
|
||||
// Redirect to the Stripe Checkout page
|
||||
window.location.href = response.stripeCheckoutUrl;
|
||||
}
|
||||
|
||||
function preventNonNumeric(event) {
|
||||
const key = event.key;
|
||||
const allowedKeys = ['Backspace', 'ArrowLeft', 'ArrowRight', 'Delete'];
|
||||
|
||||
if (!/^\d$/.test(key) && !allowedKeys.includes(key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: white;
|
||||
background-color: red;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
102
frontend/src/views/creators/ExclusiveContentCard.vue
Normal file
102
frontend/src/views/creators/ExclusiveContentCard.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<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"
|
||||
:style="{
|
||||
backgroundColor: branding.colors.surface,
|
||||
boxShadow: '0 10px 10px rgba(0, 0, 0, 0.3)',
|
||||
borderColor: `rgba(${hexToRgb(branding.colors.secondary)}, 0.4)`,
|
||||
borderWidth: '1px',
|
||||
}"
|
||||
>
|
||||
<!-- Conteneur pour aligner le titre et le bouton -->
|
||||
<div
|
||||
class="flex items-center justify-between py-2 px-3"
|
||||
:style="{ color: branding.colors.onPrimary }"
|
||||
>
|
||||
<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"
|
||||
:style="{ color: branding.colors.onPrimary }"
|
||||
>
|
||||
<i class="mdi mdi-dots-vertical text-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
<v-list
|
||||
:style="{
|
||||
backgroundColor: branding.colors.secondary,
|
||||
color: branding.colors.onSecondary,
|
||||
}"
|
||||
>
|
||||
<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"
|
||||
:style="{
|
||||
color: branding.colors.secondary,
|
||||
border: `2px solid ${branding.colors.secondary}`,
|
||||
}"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end pa-2 px-4" :style="{ color: branding.colors.onPrimary }">
|
||||
14-05-2024
|
||||
</div>
|
||||
|
||||
<div class="text-justify px-4 text-md" :style="{ color: branding.colors.onPrimary }">
|
||||
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>
|
||||
180
frontend/src/views/creators/ExclusiveContentContainer.vue
Normal file
180
frontend/src/views/creators/ExclusiveContentContainer.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<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"
|
||||
:style="{
|
||||
background: creator.colors.bannerTop,
|
||||
borderColor: `rgba(${getRGB(creator.colors.bannerBottom)}, 0.38)`
|
||||
}">
|
||||
<div class="flex justify-center items-center">
|
||||
</div>
|
||||
<div>
|
||||
<img :src="item.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="" icon 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 .v-icon {
|
||||
font-size: 18px; /* Ajustez la taille des icônes */
|
||||
}
|
||||
|
||||
.limited-text {
|
||||
height: 60px; /* Limite la hauteur du texte */
|
||||
overflow: hidden; /* Empêche le texte de dépasser */
|
||||
text-overflow: ellipsis; /* Ajoute des points de suspension si le texte dépasse */
|
||||
white-space: nowrap; /* Le texte reste sur une seule ligne */
|
||||
}
|
||||
|
||||
.stars {
|
||||
position: absolute; /* Fixe les étoiles au bas à droite */
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
58
frontend/src/views/creators/FullscreenImage.vue
Normal file
58
frontend/src/views/creators/FullscreenImage.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-dialog v-model="isVisible" fullscreen hide-overlay transition="fade-transition">
|
||||
<v-card class="pa-0" :style="{ backgroundColor: brandingStore.colors.background, color: brandingStore.colors.onBackground }">
|
||||
<v-btn
|
||||
class="close-button"
|
||||
icon
|
||||
:style="{ backgroundColor: brandingStore.colors.secondary, color: brandingStore.colors.onSecondary }"
|
||||
@click="close"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-img :src="imageUrl" class="fullscreen-image"></v-img>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
|
||||
const brandingStore = useBrandingStore();
|
||||
|
||||
const props = defineProps({
|
||||
imageUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isVisible = ref(false);
|
||||
|
||||
function open() {
|
||||
isVisible.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
isVisible.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fullscreen-image {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 50px;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
97
frontend/src/views/creators/SubscribeButton.vue
Normal file
97
frontend/src/views/creators/SubscribeButton.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<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: brandingStore.colors.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: brandingStore.colors.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"
|
||||
:style="{ border: `3px solid ${brandingStore.colors.secondary}` }">
|
||||
|
||||
<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"
|
||||
:style="{ borderColor: brandingStore.colors.secondary, color: brandingStore.colors.secondary }"
|
||||
variant="outlined"
|
||||
@click="showUnsubscribeModal = false">
|
||||
<div :style="{ color: brandingStore.colors.secondary }">Non</div>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
33
frontend/src/views/creators/SubscriptionList.vue
Normal file
33
frontend/src/views/creators/SubscriptionList.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<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>
|
||||
99
frontend/src/views/creators/SubscriptionMenu.vue
Normal file
99
frontend/src/views/creators/SubscriptionMenu.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import {ref, onMounted} from 'vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter()
|
||||
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();
|
||||
});
|
||||
|
||||
// Colors
|
||||
|
||||
const onPrimary = {color: brandingStore.colors.onPrimary}
|
||||
const Primary = {backgroundColor: brandingStore.colors.primary}
|
||||
|
||||
const onSecondaryColor = {color: brandingStore.colors.onSecondary}
|
||||
const secondaryColor = {backgroundColor: brandingStore.colors.secondary}
|
||||
|
||||
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>
|
||||
.v-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dotted-border {
|
||||
border: 2px dotted;
|
||||
padding: 1px;
|
||||
}
|
||||
</style>
|
||||
134
frontend/src/views/documentation/About.vue
Normal file
134
frontend/src/views/documentation/About.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">À propos</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
<p class="text-justify p-tos">
|
||||
Bienvenue sur la page "À Propos" d’Hutopy, où nous partageons notre histoire, notre mission,
|
||||
notre vision, et vous présentons l'équipe passionnée qui rend tout cela possible. Hutopy
|
||||
n'est pas seulement une plateforme ; c'est une communauté, un mouvement, un lieu où la
|
||||
créativité rencontre la technologie pour créer des expériences inoubliables.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">
|
||||
Notre Histoire
|
||||
</h2>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy a été fondée en 2024, née de l'idée simple mais puissante que chaque créateur qu'il
|
||||
soit grand ou petit, novice ou expérimenté, devrait avoir accès aux outils et au soutien
|
||||
nécessaires pour partager sa passion avec le monde.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">
|
||||
Notre Mission
|
||||
</h2>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
Notre mission est de démocratiser la création de contenu numérique, en offrant une
|
||||
plateforme accessible, intuitive et puissante qui permet aux créateurs de tout horizon de
|
||||
s'exprimer, d'innover et de connecter avec une audience mondiale. Nous nous engageons à
|
||||
fournir les outils, les ressources et le soutien nécessaires pour que chaque voix puisse
|
||||
être entendue.
|
||||
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">
|
||||
Notre Vision
|
||||
</h2>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
Nous envisageons un monde où la barrière entre les créateurs et leur audience est réduite au
|
||||
minimum, où les idées, l'expertise et les histoires peuvent circuler librement et sans
|
||||
entrave. Hutopy aspire à être au cœur de cet écosystème créatif et professionnel, en étant
|
||||
une source d'inspiration, une plateforme de lancement et un foyer pour tous.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">
|
||||
Notre Équipe
|
||||
</h2>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
Derrière Hutopy, il y a une équipe de penseurs innovants, de créatifs passionnés et de
|
||||
technologues dévoués, tous unis par le désir de soutenir la communauté des créateurs de
|
||||
contenu. Notre équipe est notre plus grande force, chaque membre apportant une expertise
|
||||
unique et une perspective fraîche à notre mission commune.
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<v-row justify="center">
|
||||
<v-card max-width="250px" class="card-member" style="margin: 10px;">
|
||||
<img class="member-profile-picture"
|
||||
src="/images/hutopymedia/tospage/membersPictures/profileMarco.png"
|
||||
alt="Marc-Olivier Hébert">
|
||||
<div class="card-content">
|
||||
<div class="member-name">Marc-Olivier</div>
|
||||
<div class="member-name">Hébert</div>
|
||||
<div class="member-title">Fondateur</div>
|
||||
<p class="member-description">Avec une vision claire et un engagement sans faille, il a lancé Hutopy pour changer la manière dont le contenu est créé et partagé.</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<v-card max-width="250px" class="card-member" style="margin: 10px;">
|
||||
<img class="member-profile-picture"
|
||||
src="/images/hutopymedia/tospage/membersPictures/profileDominique.png"
|
||||
alt="Dominic Villemure">
|
||||
<div class="card-content">
|
||||
<div class="member-name">Dominic</div>
|
||||
<div class="member-name">Villemure</div>
|
||||
<div class="member-title">Responsable Technique</div>
|
||||
<p class="member-description">À la tête de notre équipe de développement, il assure qu’Hutopy reste à la pointe de la technologie.</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<v-card max-width="250px" class="card-member" style="margin: 10px;">
|
||||
<img class="member-profile-picture"
|
||||
src="/images/hutopymedia/tospage/membersPictures/profilePascal.png"
|
||||
alt="Pascal Marchesseault">
|
||||
<div class="card-content">
|
||||
<div class="member-name">Pascal</div>
|
||||
<div class="member-name">Marchesseault</div>
|
||||
<div class="member-title">Gestionnaire de projet / UI</div>
|
||||
<p class="member-description">A pour mission d'assurer le développement du projet tout en créant une interface qui permettra au projet d'avoir une interaction positive et enrichissante avec Hutopy pour les utilisateurs.</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<v-card max-width="250px" class="card-member" style="margin: 10px;">
|
||||
<img class="member-profile-picture"
|
||||
src="/images/hutopymedia/tospage/membersPictures/profileChloe.png"
|
||||
alt="Chloé Beaugrand">
|
||||
<div class="card-content">
|
||||
<div class="member-name">Chloé</div>
|
||||
<div class="member-name">Beaugrand</div>
|
||||
<div class="member-title">Responsable Marketing</div>
|
||||
<p class="member-description">Elle façonne l'image d’Hutopy et engage notre communauté à travers des campagnes innovantes et impactantes.</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<v-card max-width="250px" class="card-member" style="margin: 10px;">
|
||||
<img class="member-profile-picture"
|
||||
src="/images/hutopymedia/tospage/membersPictures/Jonathan.png"
|
||||
alt="Édouard Letarte">
|
||||
<div class="card-content">
|
||||
<div class="member-name">Jonathan</div>
|
||||
<div class="member-name">Bourdon</div>
|
||||
<div class="member-title">Programeur / Architecte</div>
|
||||
<p class="member-description">Son expérience d'architecte senior nous permet de développer un logiciel avec une durabilité qui nous permettra de nous développer pendant de longues années.</p>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify py-6 p-tos">
|
||||
Chez Hutopy, nous sommes plus qu'une plateforme ; nous sommes une famille dédiée à la
|
||||
réussite de nos créateurs. Nous vous invitons à nous joindre dans cette aventure
|
||||
passionnante, à partager votre créativité et votre expertise avec le monde et à faire
|
||||
d’Hutopy votre utopie. Merci de faire partie de notre histoire.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
</style>
|
||||
131
frontend/src/views/documentation/ContentPolicy.vue
Normal file
131
frontend/src/views/documentation/ContentPolicy.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">Politique de Contenu</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
<h2 class="h2-tos">Introduction</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy vise à offrir une plateforme sécurisée, inclusive et respectueuse où les créateurs peuvent partager leur travail et interagir avec une communauté engagée. Pour maintenir cet environnement, nous avons établi des lignes directrices claires concernant le type de contenu autorisé sur notre plateforme. En utilisant Hutopy, vous acceptez de respecter cette politique de contenu.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Contenu Autorisé</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">
|
||||
Hutopy encourage la publication de contenu créatif, éducatif et inspirant dans divers formats, y compris :
|
||||
</li>
|
||||
<li class="text-justify p-tos">Arts visuels et design : Illustrations, photographies, designs graphiques respectant le droit d'auteur.</li>
|
||||
<li class="text-justify p-tos">Éducation et apprentissage : Tutoriels, cours en ligne, webinaires qui favorisent l'apprentissage et le développement personnel.</li>
|
||||
<li class="text-justify p-tos">Contenu écrit : Articles, blogs, poésies qui enrichissent les discussions et partagent des connaissances.</li>
|
||||
<li class="text-justify p-tos">Multimédia : Vidéos, podcasts et musique originales qui respectent les droits d'auteur et encouragent l'expression créative.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">Contenu Interdit</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">
|
||||
Pour protéger notre communauté, certains types de contenu ne sont pas autorisés sur Hutopy, incluant mais non limité à :
|
||||
</li>
|
||||
<li class="text-justify p-tos">Contenu illégal : Tout contenu promouvant des activités illégales ou fournissant des instructions pour commettre des actes illégaux.</li>
|
||||
<li class="text-justify p-tos">Harcèlement et discours de haine : Contenu visant à harceler, menacer, ou promouvoir la haine contre des individus ou des groupes basés sur la race, l'ethnie, la religion, le genre, l'orientation sexuelle, l'identité de genre ou tout autre caractère distinctif.</li>
|
||||
<li class="text-justify p-tos">Contenu pour adultes : Matériel pornographique ou explicitement sexuel.</li>
|
||||
<li class="text-justify p-tos">Violence et contenu graphique : Images ou descriptions de violence excessive, gore ou choquantes.</li>
|
||||
<li class="text-justify p-tos">Publicité mensongère et spam : Contenu trompeur, frauduleux ou spammy.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">Droits d'Auteur et Propriété Intellectuelle</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Respect des Droits : Vous devez posséder les droits sur le contenu que vous publiez sur Hutopy ou avoir l'autorisation expresse du détenteur des droits pour utiliser ce contenu.
|
||||
</p>
|
||||
<p class="text-justify p-tos">
|
||||
Attribution : Lorsque vous utilisez ou adaptez le contenu protégé par des droits d'auteur appartenant à autrui, une attribution claire et correcte doit être fournie.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Modération et Signalement</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Modération : Hutopy utilise à la fois des modérateurs humains et des outils automatisés pour surveiller et évaluer le contenu publié, garantissant le respect de cette politique.
|
||||
</p>
|
||||
<p class="text-justify p-tos">
|
||||
Signalement : Les utilisateurs de Hutopy sont encouragés à signaler tout contenu qu'ils considèrent comme enfreignant notre politique de contenu via les outils de signalement disponibles sur la plateforme.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Conséquences des Violations</h2>
|
||||
<p class="text-justify p-tos">
|
||||
La violation de notre politique de contenu peut entraîner des actions allant de l'avertissement à la suppression du contenu ou à la suspension, voire à la résiliation du compte utilisateur.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Révisions de la Politique</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy se réserve le droit de modifier cette politique de contenu à tout moment pour refléter les changements dans nos pratiques ou pour se conformer à de nouvelles réglementations légales.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Dans le cas d’une non conformité aux politiques de contenus :</h2>
|
||||
<p class="text-justify p-tos">
|
||||
1. Suspension des Fonds : Les montants accumulés sur le compte de l'utilisateur en question seront suspendus temporairement le temps de l'évaluation.
|
||||
</p>
|
||||
<p class="text-justify p-tos">
|
||||
2. Redistribution à des Œuvres de Charité : Si, après évaluation, le contenu est définitivement jugé non conforme à nos clauses de conformité, les fonds suspendus seront redistribués à des œuvres de charité choisies par Hutopy. L'utilisateur concerné sera informé de cette décision et des raisons de la non-conformité de son contenu.
|
||||
</p>
|
||||
<p class="text-justify p-tos">
|
||||
Cette mesure vise à renforcer la responsabilité des créateurs quant au type de contenu partagé sur Hutopy, tout en soutenant des causes bénéfiques en cas de violation de nos directives.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Section Anti-Exploitation d’Hutopy</h2>
|
||||
<h2 class="h2-tos" style="margin-top: 25px; margin-bottom: 25px; font-weight: 600; font-size: 1.3rem;">Engagement d’Hutopy</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy s'engage fermement à maintenir une plateforme sûre et respectueuse pour tous ses utilisateurs. Nous prenons une position intransigeante contre toute forme d'exploitation humaine et nous travaillons activement pour prévenir, identifier et combattre les comportements et contenus exploitants. Notre mission est de créer un environnement où la créativité et l'expression personnelle peuvent s'épanouir sans crainte d'exploitation ou d'abus.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Politique de Tolérance Zéro</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Nous appliquons une politique de tolérance zéro à l'égard de :
|
||||
</p>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Exploitation sexuelle : Cela inclut, mais n'est pas limité à, la pornographie infantile, le trafic sexuel, et le harcèlement sexuel.</li>
|
||||
<li class="text-justify p-tos">Travail forcé : Nous nous opposons à toute forme de travail forcé ou de servitude, y compris le travail des enfants.</li>
|
||||
<li class="text-justify p-tos">Exploitation financière : Cela comprend les arnaques, la fraude et tout autre type d'exploitation financière.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">Politique de Tolérance Zéro et Signalement/Actions</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Mécanismes de Signalement : Hutopy fournit des outils faciles à utiliser pour signaler rapidement tout contenu ou comportement suspect d'exploitation. Nous encourageons vivement les utilisateurs à utiliser ces outils s'ils rencontrent ou soupçonnent des cas d'exploitation.</li>
|
||||
<li class="text-justify p-tos">Réponse Rapide : Notre équipe dédiée examine tous les signalements avec la plus grande attention et prend des mesures immédiates pour adresser les problèmes signalés. Cela peut inclure la suppression de contenu, la suspension de comptes, et, si nécessaire, le signalement aux autorités compétentes.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">Collaboration avec les Autorités</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Nous collaborons étroitement avec les autorités et les organisations spécialisées pour combattre l'exploitation sous toutes ses formes. Hutopy est déterminé à respecter toutes les lois applicables et à coopérer avec les autorités dans leurs efforts de lutte contre l'exploitation et l'abus.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Engagements des Utilisateurs</h2>
|
||||
<p class="text-justify p-tos">
|
||||
En rejoignant Hutopy, les utilisateurs s'engagent à respecter nos principes anti-exploitation et à contribuer à la création d'un espace sûr pour tous. Tout manquement à ces engagements entraînera des conséquences sérieuses, conformément à notre politique de tolérance zéro.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Modération et Signalement</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Modération : Hutopy utilise à la fois des modérateurs humains et des outils automatisés pour surveiller et évaluer le contenu publié, garantissant le respect de cette politique.</li>
|
||||
<li class="text-justify p-tos">Signalement : Les utilisateurs d’Hutopy sont encouragés à signaler tout contenu qu'ils considèrent comme enfreignant notre politique de contenu via les outils de signalement disponibles sur la plateforme.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">Conséquences des Violations</h2>
|
||||
<p class="text-justify p-tos">
|
||||
La violation de notre politique de contenu peut entraîner des actions allant de l'avertissement à la suppression du contenu ou à la suspension, voire à la résiliation du compte utilisateur.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Révisions de la Politique</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy se réserve le droit de modifier cette politique de contenu à tout moment pour refléter les changements dans nos pratiques ou pour se conformer à de nouvelles réglementations légales.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Contact</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Si vous avez des questions sur cette politique de contenu ou sur la manière dont nous l'appliquons, veuillez contacter notre équipe d'assistance à <a href="mailto:support@hutopy.com">support@hutopy.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
46
frontend/src/views/documentation/CreatorGuide.vue
Normal file
46
frontend/src/views/documentation/CreatorGuide.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">Guide pour les Créateurs</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
<h2 class="h2-tos">Bienvenue dans la Communauté de Créateurs d’Hutopy</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Félicitations pour avoir choisi Hutopy pour partager votre créativité et votre savoir ! Ce guide est conçu pour vous aider à maximiser votre présence sur la plateforme, à engager votre audience et à tirer le meilleur parti des outils à votre disposition.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">1. Création de Votre Profil de Créateur :</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Personnalisez Votre Profil : Ajoutez une photo de profil, une bannière et une bio qui reflète votre personnalité et votre marque de créateur.</li>
|
||||
<li class="text-justify p-tos">Liens et Contacts : Intégrez des liens vers vos autres plateformes sociales.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">2. Publication de Contenu :</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Diversifiez Votre Contenu : Explorez différents formats – vidéos, articles, podcasts – pour captiver divers segments d'audience.</li>
|
||||
<li class="text-justify p-tos">Planification et Consistance : Publiez régulièrement pour garder votre audience engagée. Utilisez l'outil de planification d’Hutopy pour organiser vos publications à l'avance.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">3. Engagement avec Votre Audience :</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Interagissez : Répondez aux commentaires, participez à des discussions et créez des sondages pour encourager l'interaction.</li>
|
||||
<li class="text-justify p-tos">Analysez Vos Performances : Utilisez les outils d'analyse d’Hutopy pour comprendre ce qui résonne avec votre audience et ajustez votre stratégie en conséquence.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">4. Monétisation :</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Explorez les Options : Hutopy offre plusieurs voies de monétisation, y compris les abonnements payants, les dons et le programme d'ambassadeur. Choisissez ce qui convient le mieux à votre contenu et à votre audience.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h2-tos">5. Croissance et Développement :</h2>
|
||||
<ul class="list-disc pl-5">
|
||||
<li class="text-justify p-tos">Continuez à Apprendre : Utilisez le Centre de Ressources Éducatives d’Hutopy pour améliorer vos compétences et rester à jour sur les tendances du secteur. (À venir)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
8
frontend/src/views/documentation/DocumentationHome.vue
Normal file
8
frontend/src/views/documentation/DocumentationHome.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
23
frontend/src/views/documentation/DocumentationLayout.vue
Normal file
23
frontend/src/views/documentation/DocumentationLayout.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import Footer from "@/views/main/Footer.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<img src="/images/hutopymedia/banners/hutopyul.png"
|
||||
class="max-w-[1000px] rounded-2xl shadow" alt="">
|
||||
</div>
|
||||
|
||||
<div class="py-8 flex-grow">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<Footer></Footer>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
113
frontend/src/views/documentation/FAQ.vue
Normal file
113
frontend/src/views/documentation/FAQ.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">FAQ</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
|
||||
<h2 class="h2-tos">Foire Aux Questions</h2>
|
||||
<p class="text-justify p-tos">
|
||||
La section FAQ de Hutopy est votre ressource essentielle pour trouver des réponses rapides aux questions les plus
|
||||
fréquemment posées sur notre plateforme. Explorez nos réponses détaillées pour optimiser votre utilisation de
|
||||
Hutopy et résoudre vos problèmes en un instant. Consultez régulièrement notre FAQ pour rester informé des
|
||||
dernières fonctionnalités.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">1. Comment puis-je créer un compte sur Hutopy ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Créer un compte est simple ! Visitez notre page d'inscription, remplissez les informations requises, et suivez les
|
||||
instructions pour confirmer votre adresse e-mail ou vous connecter via les partenaires de connexion. Vous pourrez
|
||||
commencer à explorer et à interagir avec la communauté Hutopy immédiatement après.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">2. Quels types de contenu puis-je publier sur Hutopy ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy accueille une large variété de contenus créatifs, incluant mais non limité à des vidéos, articles,
|
||||
podcasts, et illustrations. Nous encourageons la diversité et l'originalité, tant que le contenu respecte nos
|
||||
valeurs.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">3. Comment Hutopy rémunère-t-il les créateurs de contenu ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Les créateurs peuvent monétiser leur contenu de plusieurs façons, notamment via des abonnements payants et des
|
||||
dons de la part des utilisateurs.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">4. Comment puis-je modifier mon profil ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Connectez-vous à votre compte, accédez à votre profil, puis cliquez sur "Éditer le profil" pour modifier vos
|
||||
informations, ajouter une bio, changer votre photo de profil, et plus encore.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">5. Est-il possible de supprimer mon compte ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Oui, vous pouvez faire la suppression de votre compte sur votre profil dans la section plus. Notez que cette
|
||||
action est irréversible.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">6. Que faire si j'oublie mon mot de passe ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Sur la page de connexion, cliquez sur "Mot de passe oublié ?" et suivez les instructions pour réinitialiser votre
|
||||
mot de passe via votre adresse courriel.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">7. Comment signaler un contenu inapproprié ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Si vous rencontrez du contenu qui viole nos directives, cliquer sur les trois petits points en haut de la
|
||||
publication et cliquez sur le bouton "Signaler" associé au contenu en question pour alerter notre équipe de
|
||||
modération.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">8. Comment puis-je contacter le support Hutopy ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Pour toute assistance, vous pouvez nous contacter via notre formulaire en ligne ou par e-mail à
|
||||
support@hutopy.com, ou via nos réseaux sociaux. Notre équipe s'efforce de répondre rapidement à toutes les
|
||||
demandes.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">9. Quels sont les frais pour les créateurs sur Hutopy ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy prélève une commission de 12% + 0,30$ sur chaque transaction réalisée sur la plateforme, que ce soit pour
|
||||
les abonnements, les dons ou tout autre revenu généré par les créateurs. Cette commission nous aide à couvrir les
|
||||
coûts de maintenance de la plateforme, de la bande passante, d'assistance utilisateur, des frais de transaction de
|
||||
Stripe et le développement continu pour améliorer votre expérience sur Hutopy.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">10. Y a-t-il des frais pour s'inscrire ou pour maintenir mon compte sur Hutopy ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Non, l'inscription sur Hutopy est gratuite, et il n'y a pas de frais mensuels ou annuels pour maintenir votre
|
||||
compte. Vous pouvez commencer à utiliser Hutopy et à partager votre contenu sans aucun coût initial.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">11. Les utilisateurs doivent-ils payer pour accéder au contenu sur Hutopy ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy offre à la fois du contenu gratuit et du contenu premium. Les utilisateurs peuvent accéder gratuitement à
|
||||
une partie du contenu sur la plateforme. Cependant, certains créateurs peuvent choisir de rendre leur contenu
|
||||
accessible uniquement via un abonnement payant ou un achat unique pour soutenir leur travail.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">12. Existe-t-il des frais pour retirer mes gains de la plateforme ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Les créateurs peuvent retirer leurs gains sans frais supplémentaires de la part d’Hutopy. Cependant, les
|
||||
transactions bancaires ou les transferts vers des portefeuilles électroniques peuvent être soumis aux frais
|
||||
standards imposés par ces services ou institutions financières, mais pas par Hutopy.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">13. Les frais Hutopy sont-ils les mêmes pour tous les types de contenu ?</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Oui, les frais de commission d’Hutopy sont uniformément appliqués à tous les types de contenu et de transactions
|
||||
sur la plateforme pour maintenir la simplicité et la transparence et ce peu importe le montant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
|
||||
.important {
|
||||
@apply m-2 text-red-500 my-4;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
42
frontend/src/views/documentation/HelpAndContact.vue
Normal file
42
frontend/src/views/documentation/HelpAndContact.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">Aide et contact</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
<p class="text-justify p-tos">
|
||||
Bienvenue dans notre espace d'assistance ! Que vous soyez un créateur à la recherche de conseils pour optimiser votre présence sur Hutopy, ou un utilisateur curieux d'en apprendre plus sur notre plateforme, vous êtes au bon endroit. Notre objectif est de vous fournir tout le soutien nécessaire pour que votre expérience sur Hutopy soit aussi enrichissante et agréable que possible.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">FAQ (Foire Aux Questions)</h2>
|
||||
<p class="text-justify">
|
||||
Retrouvez les réponses aux questions les plus fréquemment posées concernant l'utilisation d’Hutopy, les fonctionnalités de la plateforme, les options de monétisation, et plus encore. Consulter la FAQ
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Contactez-Nous</h2>
|
||||
<p class="text-justify">
|
||||
Nous sommes toujours ravis d'entendre nos utilisateurs ! Que ce soit pour partager vos retours, poser une question spécifique, ou demander des renseignements sur des partenariats, n'hésitez pas à nous contacter.
|
||||
</p>
|
||||
<p class="text-justify">
|
||||
- Par E-mail : <a href="mailto:info@hutopy.com" style="color: #a30e79;">info@hutopy.com</a><br>
|
||||
- Réseaux Sociaux : Nous sommes actifs sur <a href="https://www.facebook.com/Hutopy" style="color: #a30e79;">Facebook</a>, et <a href="https://www.instagram.com/hutopy.inc" style="color: #a30e79;">Instagram</a><br>
|
||||
- Suivez-nous pour rester informé et interagir avec notre communauté.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Assistance Technique</h2>
|
||||
<p class="text-justify">
|
||||
Rencontrez-vous un problème technique ?
|
||||
Notre équipe d'assistance est là pour vous aider : <a href="mailto:support@hutopy.com" style="color: #a30e79;">support@hutopy.com</a>
|
||||
</p>
|
||||
<p class="text-justify my-5">
|
||||
Nous sommes là pour rendre votre expérience sur Hutopy aussi fluide et positive que possible. N'hésitez pas à nous contacter pour toute aide supplémentaire !
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
27
frontend/src/views/documentation/Pricing.vue
Normal file
27
frontend/src/views/documentation/Pricing.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">Frais</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
<p class="text-justify p-tos">
|
||||
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 afin de développer des fonctionnalités innovantes, maintenir une infrastructure technologique de pointe, et un support utilisateur de premier ordre. Notre objectif ? Amplifier votre expansion et garantir une expérience utilisateur sans précédent.
|
||||
</p>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
Pour chaque transaction, un frais minime assure la sécurité et la fiabilité des paiements, grâce à un partenaire de confiance mondialement reconnu. Ce dernier sécurise des milliards en transactions chaque année pour une diversité d'entreprises, à des entreprises en démarrage aux conglomérats établis. Ce gage de sécurité est disponible pour une somme de 2,9 % plus un 0,30 $ par transaction, une petite contribution pour la tranquillité d'esprit et la protection de vos revenus.
|
||||
</p>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
Notre modèle tarifaire, pensé pour la simplicité et la transparence, a pour 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 et ceux de vos donnateurs sont précieusement gardés.
|
||||
</p>
|
||||
|
||||
<p class="text-justify p-tos">
|
||||
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, 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.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
</style>
|
||||
71
frontend/src/views/documentation/TermsAndConditions.vue
Normal file
71
frontend/src/views/documentation/TermsAndConditions.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="h1-tos py-10 flex items-center justify-center uppercase">Conditions générales</h1>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1000px] px-4">
|
||||
<h2 class="h2-tos">Bienvenue sur Hutopy</h2>
|
||||
<p class="text-justify p-tos">
|
||||
En accédant à la plateforme Hutopy et en l'utilisant, vous acceptez de vous conformer aux conditions générales d'utilisation suivantes, qui sont conçues pour assurer une expérience sûre, respectueuse et positive pour tous les utilisateurs. Ces conditions s'appliquent à tous les visiteurs, utilisateurs et autres personnes qui accèdent ou utilisent le service.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Utilisation Acceptable</h2>
|
||||
<p class="text-justify p-tos">
|
||||
1. Contenu : Vous vous engagez à ne pas publier de contenu illégal, diffamatoire, abusif, pornographique, haineux, raciste ou de toute autre nature susceptible de causer du tort. Tout contenu publié reste sous votre responsabilité.
|
||||
</p>
|
||||
<p class="text-justify p-tos">
|
||||
2. Comportement : Tout comportement visant à nuire à d'autres utilisateurs, à la plateforme ou à ses opérations est strictement interdit. Cela inclut le piratage, la diffusion de logiciels malveillants et les tentatives d'hameçonnage.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Droits de Propriété Intellectuelle</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Le contenu publié sur Hutopy par les utilisateurs reste la propriété de leurs créateurs respectifs. En publiant du contenu sur Hutopy, vous accordez à la plateforme une licence non exclusive, transférable, libre de droits et mondiale pour utiliser, reproduire, modifier, publier, traduire et distribuer ce contenu dans tout média.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Confidentialité</h2>
|
||||
<p class="text-justify p-tos">
|
||||
La protection de vos données personnelles est de la plus haute importance pour Hutopy. Votre information est collectée et utilisée conformément à notre politique de confidentialité.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Limitation de Responsabilité</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy et ses affiliés ne seront pas responsables des dommages indirects, accidentels, spéciaux, consécutifs ou punitifs, y compris sans limitation, la perte de profits, de données ou d'usage, que ce soit dans une action contractuelle, délictuelle y compris la négligence ou autre, découlant de ou en relation avec l'accès ou l'utilisation de la plateforme Hutopy.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Clause de Non-Poursuite</h2>
|
||||
<p class="text-justify p-tos">
|
||||
En acceptant ces conditions générales d'utilisation, vous convenez qu'en aucun cas Hutopy, ses dirigeants, employés, partenaires, agents, fournisseurs ou affiliés ne pourront être tenus responsables de dommages directs, indirects, accidentels, spéciaux, consécutifs ou exemplaires résultant de votre utilisation de la plateforme Hutopy. Par conséquent, vous renoncez expressément à tout droit de poursuivre Hutopy et ses affiliés pour toute réclamation liée à votre utilisation de la plateforme.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Gestion du Contenu Inapproprié et Sanctions Financières</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy s'engage à maintenir un environnement sûr et respectueux pour tous ses utilisateurs. Ainsi, tout contenu publié sur la plateforme est sujet à une évaluation de conformité avec nos directives et nos standards éthiques. Dans l'éventualité où le contenu d'un utilisateur est jugé inapproprié, offensant ou en violation avec nos Acceptation des Conditions
|
||||
<br><br>
|
||||
Votre accès et votre utilisation continue de la plateforme Hutopy constituent votre acceptation des présentes conditions générales et de toutes les modifications futures. Il est de votre responsabilité de vous tenir informé des mises à jour de ces conditions.
|
||||
<br><br>
|
||||
Nous vous encourageons à utiliser Hutopy de manière responsable et conforme à nos directives, afin de contribuer à une communauté positive et enrichissante pour tous.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Modifications des Conditions</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy se réserve le droit de modifier ou de remplacer ces conditions à tout moment. Il est de votre responsabilité de revoir régulièrement ces conditions pour vous tenir informé des mises à jour.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Résiliation</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Hutopy peut résilier ou suspendre votre accès à la plateforme immédiatement, sans préavis ni responsabilité, pour quelque raison que ce soit, y compris, sans limitation, si vous violez les conditions.
|
||||
</p>
|
||||
|
||||
<h2 class="h2-tos">Loi Applicable</h2>
|
||||
<p class="text-justify p-tos">
|
||||
Ces conditions seront régies et interprétées conformément aux lois du pays/juridiction où est basée la plateforme, sans égard à ses conflits de dispositions légales.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@/cssstyle/tosstyle.css';
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
78
frontend/src/views/main/DonationPopup.vue
Normal file
78
frontend/src/views/main/DonationPopup.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="fixed z-50 bottom-6 right-6 flex flex-column">
|
||||
<div
|
||||
v-if="showPopup"
|
||||
ref="popup"
|
||||
class="z-50 shadow-md shadow-gray-500 rounded-2xl"
|
||||
>
|
||||
<div class="bg-fuchsia-900 p-4 rounded-t-2xl font-semibold self-center text-white text-center">
|
||||
Je Soutiens!
|
||||
</div>
|
||||
<div class="bg-gray-100 rounded-b-2xl p-4">
|
||||
<div class="mx-2">
|
||||
<StripePayment :creator-id="creatorId"></StripePayment>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="togglePopup"
|
||||
ref="popupButton"
|
||||
class="bg-purple rounded-full w-16 h-16 flex justify-center items-center self-end mt-4 cursor-pointer"
|
||||
style="background: radial-gradient(circle, rgba(163,14,121,1) 50%, rgba(107,0,101,1) 100%); border: 2px solid white;"
|
||||
>
|
||||
<v-icon class="text-2xl">mdi-gift-outline</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onUnmounted} from 'vue';
|
||||
import StripePayment from "@/views/StripePayment.vue";
|
||||
|
||||
const showPopup = ref(false);
|
||||
const popup = ref(null);
|
||||
const popupButton = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
creatorId: {type: String, required: true},
|
||||
});
|
||||
|
||||
|
||||
const togglePopup = () => {
|
||||
showPopup.value = !showPopup.value;
|
||||
};
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
popup.value &&
|
||||
!popup.value.contains(event.target) &&
|
||||
!popupButton.value.contains(event.target) &&
|
||||
!event.target.closest('.bg-purple')
|
||||
) {
|
||||
showPopup.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-fuchsia-900 {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.bg-purple {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
</style>
|
||||
106
frontend/src/views/main/Footer.vue
Normal file
106
frontend/src/views/main/Footer.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import XIcon from '@/assets/icons/x.svg'
|
||||
import FacebookIcon from '@/assets/icons/facebook.svg'
|
||||
import InstagramIcon from '@/assets/icons/instagram.svg'
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
const brandingStore = useBrandingStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<footer class="py-8 flex flex-col gap-8" :style="{color: brandingStore.colors.onBackground}">
|
||||
|
||||
<div class="centered-text text-2xl font-bold flex justify-center items-center ml-28 lg:tracking-[125px] md:tracking-[50px] sm:tracking-[20px]">Hutopy</div>
|
||||
|
||||
|
||||
<div class="flex flex-row justify-center gap-10">
|
||||
<a href="https://www.facebook.com/profile.php?id=61556819217561">
|
||||
<facebook-icon class="icon" :style="{ fill: brandingStore.colors.onBackground }" ></facebook-icon>
|
||||
</a>
|
||||
|
||||
<a href="https://www.instagram.com/hutopy.inc/">
|
||||
<instagram-icon class="icon" :style="{ fill: brandingStore.colors.onBackground }"></instagram-icon>
|
||||
</a>
|
||||
|
||||
<a href="https://x.com/Hutopyinc/">
|
||||
<x-icon class="icon" :style="{ fill: brandingStore.colors.onBackground }"></x-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row flex-wrap justify-center gap-4 px-4 " >
|
||||
<router-link to="/documents/helpandcontact" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.helpandcontact') }}
|
||||
</router-link>
|
||||
<router-link to="/documents/faq" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.faq') }}
|
||||
</router-link>
|
||||
<router-link to="/documents/guideforcreators" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.creatorguide') }}
|
||||
</router-link>
|
||||
<router-link to="/documents/termsandconditions" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.termsandconditions') }}
|
||||
</router-link>
|
||||
<router-link to="/documents/contentpolicy" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.contentpolicy') }}
|
||||
</router-link>
|
||||
<router-link to="/documents/about" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.about') }}
|
||||
</router-link>
|
||||
<router-link to="/documents/pricing" :style="{color: brandingStore.colors.onBackground}">
|
||||
{{ $t('footer.pricing') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center base-text mb-13" :style="{color: brandingStore.colors.onBackground}">
|
||||
Hutopy ©{{ new Date().getFullYear() }} - {{ $t('footer.allRightsReserved') }}
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.base-text {
|
||||
@apply text-gray-600 tracking-widest font-sans text-sm uppercase
|
||||
}
|
||||
|
||||
a {
|
||||
@apply base-text
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@apply text-gray-400
|
||||
}
|
||||
|
||||
.centered-text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
letter-spacing: 125px;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-left: 7rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.centered-text {
|
||||
letter-spacing: 60px;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.centered-text {
|
||||
letter-spacing: 40px;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
91
frontend/src/views/main/Home.vue
Normal file
91
frontend/src/views/main/Home.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<!-- <div class="bg-gray-100">-->
|
||||
<!-- <div class="py-6">-->
|
||||
<!-- <div class=" mx-auto flex justify-center">-->
|
||||
<!-- <img src="/images/hutopymedia/banners/hutopy.png" alt="Hutopy Logo" class="h-24">-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="mx-auto flex justify-center pt-10 max-w-[980px]">-->
|
||||
<!-- <img src="/images/hutopymedia/homepage/bannierehomepage.png" alt="Create CallToAction"-->
|
||||
<!-- class="max-w-full block rounded-none md:rounded-2xl">-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div>-->
|
||||
<!-- <div-->
|
||||
<!-- class="mx-auto flex flex-col md:flex-row justify-center max-w-[1000px] space-y-2 md:space-x-4 md:space-y-0 py-5">-->
|
||||
<!-- <div class="relative group w-full max-w-[250px] md:max-w-[306px] rounded-2xl overflow-hidden mx-auto">-->
|
||||
<!-- <img src="/images/hutopymedia/homepage/creer.png" alt="Create CallToAction" class="w-full rounded-2xl">-->
|
||||
<!-- <div-->
|
||||
<!-- class="absolute inset-0 bg-fuchsia-600 bg-opacity-0 group-hover:bg-opacity-80 flex items-center justify-center transition duration-300">-->
|
||||
<!-- <p class="text-white text-lg opacity-0 group-hover:opacity-100 transition duration-300 m-3 text-justify">-->
|
||||
<!-- Libérez votre créativité sur Hutopy, où chaque idée trouve sa place et chaque créateur détient la clé d'un-->
|
||||
<!-- monde rempli de possibilités infinies. Rejoignez-nous et transformez votre passion en réalité.-->
|
||||
<!-- </p>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="relative group w-full max-w-[250px] md:max-w-[306px] rounded-2xl overflow-hidden mx-auto">-->
|
||||
<!-- <img src="/images/hutopymedia/homepage/partager.png" alt="Share CallToAction" class="w-full rounded-2xl">-->
|
||||
<!-- <div-->
|
||||
<!-- class="absolute inset-0 bg-fuchsia-600 bg-opacity-0 group-hover:bg-opacity-80 flex items-center justify-center transition duration-300">-->
|
||||
<!-- <p class="text-white text-lg opacity-0 group-hover:opacity-100 transition duration-300 m-3 text-justify">-->
|
||||
<!-- Plongez dans l'univers Hutopy et découvrez un espace où profiter rime avec s'enrichir. Savourez des contenus-->
|
||||
<!-- uniques, des interactions authentiques et une expérience personnalisée conçue pour éveiller vos sens et-->
|
||||
<!-- enrichir votre quotidien.-->
|
||||
<!-- </p>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="relative group w-full max-w-[250px] md:max-w-[306px] rounded-2xl overflow-hidden mx-auto">-->
|
||||
<!-- <img src="/images/hutopymedia/homepage/inspirer.png" alt="Inspire CallToAction" class="w-full rounded-2xl">-->
|
||||
<!-- <div-->
|
||||
<!-- class="absolute inset-0 bg-fuchsia-600 bg-opacity-0 group-hover:bg-opacity-80 flex items-center justify-center transition duration-300">-->
|
||||
<!-- <p class="text-white text-lg opacity-0 group-hover:opacity-100 transition duration-300 m-3 text-justify">-->
|
||||
<!-- Devenez une source d'inspiration sur Hutopy, en partageant votre vision, votre talent et vos histoires.-->
|
||||
<!-- Influencez positivement la communauté, éveillez la curiosité et inspirez les autres à poursuivre leurs rêves-->
|
||||
<!-- dans un cercle vertueux de créativité et d'inspiration.-->
|
||||
<!-- </p>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div>
|
||||
|
||||
<!-- Main Content Section -->
|
||||
<div class="max-w-4xl mx-auto px-6 py-8 space-y-6">
|
||||
<img src="/images/hutopymedia/homepage/votrehutopy.png" alt="YourHutopy" class="mx-auto mb-8">
|
||||
|
||||
<div>
|
||||
<div class="space-y-4">
|
||||
<p class="text-lg leading-relaxed text-justify">
|
||||
Notre plateforme offre à ses utilisateurs un espace dédié pour centraliser leurs réseaux sociaux sur leur page personnelle. Grâce à cette fonctionnalité, chaque utilisateur peut rediriger son audience vers ses différents comptes et sites web de manière simple et efficace.
|
||||
|
||||
De plus, notre site permet aux créateurs de recevoir des donations directement via leur page, leur offrant un moyen supplémentaire de soutien financier.
|
||||
|
||||
Ainsi, les utilisateurs peuvent facilement connecter leurs visiteurs à leur univers digital tout en renforçant leur communauté et en augmentant les interactions sur leurs autres plateformes de réseaux sociaux.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.overlay p {
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
</style>
|
||||
33
frontend/src/views/main/LoginForm.vue
Normal file
33
frontend/src/views/main/LoginForm.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center min-w-[300px] m-4">
|
||||
<h1 class="text-center text-2xl font-bold mb-5">Connexion</h1>
|
||||
|
||||
<google-login class="w-full"
|
||||
:callback="googleCallback"
|
||||
popup-type="TOKEN">
|
||||
<v-btn density="comfortable" class="mb-2 w-full">
|
||||
<v-icon left>mdi-google</v-icon>
|
||||
Google
|
||||
</v-btn>
|
||||
</google-login>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {useAuthStore} from '@/stores/authStore.js';
|
||||
import {GoogleLogin} from "vue3-google-login";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const errorSnackBar = ref(false);
|
||||
|
||||
async function googleCallback(token) {
|
||||
const response = await authStore.loginWithGoogle(JSON.stringify(token));
|
||||
if (response !== true) {
|
||||
errorSnackBar.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
159
frontend/src/views/main/SideBar.vue
Normal file
159
frontend/src/views/main/SideBar.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup>
|
||||
import SubscriptionList from "@/views/creators/SubscriptionList.vue";
|
||||
import { useAuthStore } from "@/stores/authStore.js";
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useCreatorProfileStore } from "@/stores/creatorProfileStore.js";
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
import {useSideBarStore} from "@/stores/sideBarStore.js";
|
||||
|
||||
const {locale} = useI18n();
|
||||
const router = useRouter();
|
||||
const selectedLanguage = ref(locale.value);
|
||||
|
||||
const userProfileStore = useUserProfileStore();
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const authStore = useAuthStore();
|
||||
const sideBarStore = useSideBarStore();
|
||||
|
||||
const creatorIsCurrentUser = computed(() => authStore.isAuthenticated && authStore.userId);
|
||||
|
||||
const createHtmlContent = () => {
|
||||
router.push('/content/editor');
|
||||
};
|
||||
|
||||
function initializeLocale() {
|
||||
const preferredLocale = localStorage.getItem('preferredLocale');
|
||||
selectedLanguage.value = preferredLocale === null ? locale.value : preferredLocale;
|
||||
locale.value = selectedLanguage.value;
|
||||
}
|
||||
|
||||
function toggleLanguage() {
|
||||
const lang = selectedLanguage.value === 'fr' ? 'en' : 'fr';
|
||||
locale.value = lang;
|
||||
selectedLanguage.value = lang;
|
||||
localStorage.setItem('preferredLocale', lang);
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
sideBarStore.toggle();
|
||||
}
|
||||
|
||||
initializeLocale();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav :class="['fixed flex flex-col h-full bg-white border-r border-gray-300', sideBarStore.isOpen ? 'max-w-64 px-4' : 'max-w-[82px] px-2']">
|
||||
<!-- LOGO HUTOPY -->
|
||||
<div class="mt-4" :class="sideBarStore.isOpen ? 'px-4' : 'px-2'">
|
||||
<router-link to="/@hutopy">
|
||||
<img v-if="sideBarStore.isOpen"
|
||||
src="/images/hutopymedia/banners/hutopy.png"
|
||||
alt="hutopy"
|
||||
width="300px"
|
||||
height="64px">
|
||||
<img v-else
|
||||
src="/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png"
|
||||
alt="hutopy"
|
||||
width="42px"
|
||||
height="42px">
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <div class="flex-grow mt-4" :class="sideBarStore.isOpen ? 'px-4' : 'px-2'">-->
|
||||
<!-- <template v-if="authStore.isAuthenticated"> -->
|
||||
<!-- <div class="font-bold" :class="{ 'text-center': !sideBarStore.isOpen }">-->
|
||||
<!-- <span v-if="sideBarStore.isOpen">{{ $t('sidebar.subscriptionTitle') }}</span>-->
|
||||
<!-- <span v-else>A</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div-->
|
||||
<!-- class="border-b border-gray-300 my-4 mx-auto"-->
|
||||
<!-- :class="sideBarStore.isOpen ? 'w-48' : 'w-16'"-->
|
||||
<!-- ></div>-->
|
||||
<!-- <subscription-list v-if="sideBarStore.isOpen"></subscription-list>-->
|
||||
<!-- </template>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div
|
||||
class="border-b border-gray-300 my-4 mx-auto"
|
||||
:class="sideBarStore.isOpen ? 'w-48' : 'w-16'"
|
||||
></div>
|
||||
|
||||
<div class="flex-grow"></div>
|
||||
|
||||
<!-- SECTION UTILISATEUR -->
|
||||
<div :class="sideBarStore.isOpen ? 'px-4' : 'px-2'">
|
||||
<div class="flex items-center justify-start p-2 mb-4" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<img
|
||||
:src="userProfileStore.portraitUrl"
|
||||
alt="Profile Image"
|
||||
referrerpolicy="no-referrer"
|
||||
class="rounded-full"
|
||||
width="42"
|
||||
height="42"
|
||||
style="max-height: 42px;">
|
||||
<span v-if="sideBarStore.isOpen" class="ml-2 text-sm font-sans capitalize">
|
||||
{{ userProfileStore.alias }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<!-- <router-link v-if="creatorProfileStore.hasCreator" :to="`/content/editor`">-->
|
||||
<!-- <v-btn class="w-full justify-start" prepend-icon="mdi-pencil" variant="flat" :class="!sideBarStore.isOpen ? 'my-2' : ''">-->
|
||||
<!-- <span v-if="sideBarStore.isOpen">Éditeur</span>-->
|
||||
<!-- </v-btn>-->
|
||||
<!-- </router-link>-->
|
||||
|
||||
<router-link v-if="creatorProfileStore.hasCreator" :to="`/@${creatorProfileStore.creator.name}`">
|
||||
<v-btn class="w-full justify-start" prepend-icon="mdi-home-account" variant="flat" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<span v-if="sideBarStore.isOpen">{{ creatorProfileStore.creator.name }}</span>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<router-link v-else-if="authStore.isAuthenticated"
|
||||
class="w-full justify-start"
|
||||
to="/create-creator">
|
||||
<v-btn class="w-full" variant="plain" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<span v-if="sideBarStore.isOpen">Activer votre page</span>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<div v-if="authStore.isAuthenticated">
|
||||
<v-btn to="/profile" class="w-full justify-start" prepend-icon="mdi-account" variant="flat" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<span v-if="sideBarStore.isOpen">{{ $t('header.myprofile') }}</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-btn variant="flat" class="w-full justify-start" prepend-icon="mdi-translate-variant" @click="toggleLanguage" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<span v-if="sideBarStore.isOpen">{{ $t('language.language') }}</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@click="toggleMenu"
|
||||
variant="flat"
|
||||
class="w-full justify-start"
|
||||
:prepend-icon="sideBarStore.isOpen ? 'mdi-arrow-collapse-left' : 'mdi-arrow-collapse-right'"
|
||||
:class="!sideBarStore.isOpen ? 'my-2' : ''"
|
||||
>
|
||||
<span v-if="sideBarStore.isOpen">{{ $t('sidebar.Reduce') }}</span>
|
||||
</v-btn>
|
||||
|
||||
<div v-if="!authStore.isAuthenticated">
|
||||
<v-btn to="/login" variant="flat" class="justify-start" prepend-icon="mdi-login" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<span v-if="sideBarStore.isOpen">{{ $t('sidebar.connection') }}</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-btn @click="authStore.logout" variant="flat" class="justify-start" prepend-icon="mdi-logout" :class="!sideBarStore.isOpen ? 'my-2' : ''">
|
||||
<span v-if="sideBarStore.isOpen">{{ $t('header.Signout') }}</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
121
frontend/src/views/main/Wallet.vue
Normal file
121
frontend/src/views/main/Wallet.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<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>
|
||||
186
frontend/src/views/messages/Message.vue
Normal file
186
frontend/src/views/messages/Message.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<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>
|
||||
96
frontend/src/views/messages/MessageList.vue
Normal file
96
frontend/src/views/messages/MessageList.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<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>
|
||||
113
frontend/src/views/messages/PostMessage.vue
Normal file
113
frontend/src/views/messages/PostMessage.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<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>
|
||||
147
frontend/src/views/profile/ProfilePage.vue
Normal file
147
frontend/src/views/profile/ProfilePage.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<!-- Mobile -->
|
||||
<div v-if="isMobileView" class="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6 text-center">
|
||||
<!-- Image -->
|
||||
<img src="/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png" alt="Image" class="w-64 h-64 rounded-full mb-4 border" />
|
||||
|
||||
<!-- Message -->
|
||||
<div class="text-lg text-gray-700 mt-8">
|
||||
<p class="font-semibold mb-2">Pour vous connecter et modifier votre page, veuillez utiliser un appareil avec un écran plus large, comme un ordinateur.</p>
|
||||
<p>Pour le moment, l'expérience sur téléphone n'est pas encore complétée.</p>
|
||||
<p class="mt-4 font-bold">Désolé de l'inconvénient.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PC -->
|
||||
<div v-else>
|
||||
<div class="flex flex-col md:flex-row bg-[#f4f4f4] h-full">
|
||||
<!-- Left Menu -->
|
||||
<div class=" z-20 w-full md:max-w-xs fixed md:sticky md:top-0 md:flex md:flex-col top-0">
|
||||
<div class="sticky top-20 z-30">
|
||||
<div class="flex flex-col items-center md:items-start md:pl-4 mt-16">
|
||||
<h1 class="text-2xl py-4 font-bold text-center md:text-left">{{$t('profilemenu.manageyouraccount')}}</h1>
|
||||
|
||||
<div class="relative flex items-center md:mt-0 w-full">
|
||||
<!-- Navigation buttons for small screens -->
|
||||
<button @click="scrollLeftFunc"
|
||||
class="rounded p-1 absolute left-2 z-10 md:hidden text-fuchsia-800 text-2xl ">
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="flex md:flex-col space-x-2 space-y-0 md:space-x-0 md:space-y-2 p-4 items-center md:items-start overflow-x-scroll md:overflow-x-visible mx-2 md:mx-0 custom-scroll min-w-[400px] px-1"
|
||||
@mousedown="mouseDown"
|
||||
@mouseleave="mouseLeave"
|
||||
@mouseup="mouseUp"
|
||||
@mousemove="mouseMove">
|
||||
|
||||
<v-btn variant="text" @click="currentComponent = 'CreatorPage'">
|
||||
<v-icon class="mr-2">mdi-file-edit-outline</v-icon>
|
||||
{{ $t('profilemenu.creator') }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="text" @click="currentComponent = 'AccountPage'">
|
||||
<v-icon class="mr-2">mdi-information</v-icon>
|
||||
{{ $t('profilemenu.user') }}
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
<button @click="scrollRightFunc"
|
||||
class="rounded p-1 absolute right-2 z-10 md:hidden text-fuchsia-800 bg-[#f4f4f4] text-2xl">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mid Content -->
|
||||
<div class="flex flex-col flex-1 align-center py-12 p-3 mt-28 md:mt-0">
|
||||
<template v-if="currentComponent === 'CreatorPage'">
|
||||
<creator-page></creator-page>
|
||||
</template>
|
||||
<template v-else-if="currentComponent === 'AccountPage'">
|
||||
<account-page></account-page>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import CreatorPage from "@/views/profile/creators/CreatorPage.vue";
|
||||
import AccountPage from "@/views/profile/account/AccountPage.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
const { smAndDown } = useDisplay();
|
||||
|
||||
const route = useRoute();
|
||||
const startingComponent = route.query.target || 'CreatorPage';
|
||||
const currentComponent = ref(startingComponent);
|
||||
|
||||
const isMobileView = ref(smAndDown.value);
|
||||
|
||||
watch(smAndDown, (newVal) => {
|
||||
isMobileView.value = newVal;
|
||||
});
|
||||
|
||||
// Gestion du slider (scroll sur petit écran)
|
||||
const isDown = ref(false);
|
||||
const startX = ref(0);
|
||||
const scrollLeft = ref(0);
|
||||
|
||||
const mouseDown = (e) => {
|
||||
const slider = document.querySelector('.custom-scroll');
|
||||
isDown.value = true;
|
||||
slider.classList.add('active');
|
||||
startX.value = e.pageX - slider.offsetLeft;
|
||||
scrollLeft.value = slider.scrollLeft;
|
||||
};
|
||||
|
||||
const mouseLeave = () => {
|
||||
isDown.value = false;
|
||||
const slider = document.querySelector('.custom-scroll');
|
||||
slider.classList.remove('active');
|
||||
};
|
||||
|
||||
const mouseUp = () => {
|
||||
isDown.value = false;
|
||||
const slider = document.querySelector('.custom-scroll');
|
||||
slider.classList.remove('active');
|
||||
};
|
||||
|
||||
const mouseMove = (e) => {
|
||||
if (!isDown.value) return;
|
||||
e.preventDefault();
|
||||
const slider = document.querySelector('.custom-scroll');
|
||||
const x = e.pageX - slider.offsetLeft;
|
||||
const walk = (x - startX.value) * 3; // scroll-fast
|
||||
slider.scrollLeft = scrollLeft.value - walk;
|
||||
};
|
||||
|
||||
const scrollLeftFunc = () => {
|
||||
const container = document.querySelector('.custom-scroll');
|
||||
container.scrollBy({ left: -100, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const scrollRightFunc = () => {
|
||||
const container = document.querySelector('.custom-scroll');
|
||||
container.scrollBy({ left: 100, behavior: 'smooth' });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.custom-scroll {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
</style>
|
||||
299
frontend/src/views/profile/account/AccountPage.vue
Normal file
299
frontend/src/views/profile/account/AccountPage.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center w-[800px] gap-4">
|
||||
|
||||
<h1 class="uppercase pb-5 text-2xl">
|
||||
<v-icon class="mr-2">mdi-information</v-icon>
|
||||
{{ $t('personnalinformation.title') }}
|
||||
</h1>
|
||||
|
||||
<v-card class="w-full">
|
||||
<v-card-title>
|
||||
{{ $t('personnalinformation.informations') }}
|
||||
</v-card-title>
|
||||
|
||||
<!-- <button-->
|
||||
<!-- class="editableValue"-->
|
||||
<!-- @click="openEditPortrait">-->
|
||||
<!-- <span class="label">{{ $t('personnalinformation.profilepicture') }}</span>-->
|
||||
<!-- <span class="value">Un portrait vous permet de personnaliser votre profil</span>-->
|
||||
<!-- <span>-->
|
||||
<!-- <img-->
|
||||
<!-- :src="userProfileStore.user.portraitUrl"-->
|
||||
<!-- alt="Profile Image"-->
|
||||
<!-- class="rounded-full"-->
|
||||
<!-- width="48px"-->
|
||||
<!-- height="48px"/>-->
|
||||
<!-- </span>-->
|
||||
<!-- </button>-->
|
||||
|
||||
<button
|
||||
class="editableValue"
|
||||
@click="openEditFullname">
|
||||
<span class="label">{{ $t('personnalinformation.fullname') }}</span>
|
||||
<span class="value">{{ userProfileStore.fullname }}</span>
|
||||
<span><v-icon>mdi-chevron-right</v-icon></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="editableValue"
|
||||
@click="openEditAlias">
|
||||
<span class="label">{{ $t('personnalinformation.alias') }}</span>
|
||||
<span class="value">{{ userProfileStore.user.alias }}</span>
|
||||
<span><v-icon>mdi-chevron-right</v-icon></span>
|
||||
</button>
|
||||
|
||||
<!-- <button-->
|
||||
<!-- class="editableValue"-->
|
||||
<!-- @click="openEditBirthday">-->
|
||||
<!-- <span class="label">{{ $t('personnalinformation.dob') }}</span>-->
|
||||
<!-- <span class="value">{{ userProfileStore.user.birthDate }}</span>-->
|
||||
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
||||
<!-- </button>-->
|
||||
|
||||
</v-card>
|
||||
|
||||
<!-- Phone & email -->
|
||||
<v-card class="w-full">
|
||||
<v-card-title>
|
||||
{{ $t('personnalinformation.contactdetails') }}
|
||||
</v-card-title>
|
||||
|
||||
<button
|
||||
class="editableValue"
|
||||
@click="openEditEmail">
|
||||
<span class="label">{{ $t('personnalinformation.email') }}</span>
|
||||
<span class="value">{{ userProfileStore.user.email }}</span>
|
||||
<span><v-icon>mdi-chevron-right</v-icon></span>
|
||||
</button>
|
||||
|
||||
<!-- <button-->
|
||||
<!-- class="editableValue"-->
|
||||
<!-- @click="openEditPhone">-->
|
||||
<!-- <span class="label">{{ $t('personnalinformation.phone') }}</span>-->
|
||||
<!-- <span class="value">{{ userProfileStore.user.phoneNumber }}</span>-->
|
||||
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
||||
<!-- </button>-->
|
||||
</v-card>
|
||||
|
||||
<!-- Address -->
|
||||
<!-- <v-card class="w-full">-->
|
||||
<!-- <v-card-title>-->
|
||||
<!-- {{ $t('personnalinformation.addresses') }}-->
|
||||
<!-- </v-card-title>-->
|
||||
|
||||
<!-- <button-->
|
||||
<!-- class="editableValue"-->
|
||||
<!-- @click="openEditAddress">-->
|
||||
<!-- <span class="label">{{ $t('personnalinformation.home') }}</span>-->
|
||||
<!-- <span class="value">{{ userProfileStore.user.address }}</span>-->
|
||||
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
||||
<!-- </button>-->
|
||||
<!-- </v-card>-->
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<v-dialog v-model="dialogEditPortraitShown" max-width="600px">
|
||||
<portrait-dialog
|
||||
:portrait-url="userProfileStore.user.portraitUrl"
|
||||
@close="handleCloseEditPortrait"
|
||||
@save="handleSaveEditPortrait"
|
||||
></portrait-dialog>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialogEditFullnameShown" max-width="600px">
|
||||
<fullname-dialog
|
||||
:firstname="userProfileStore.user.firstname"
|
||||
:lastname="userProfileStore.user.lastname"
|
||||
@close="handleCloseEditFullname"
|
||||
@save="handleSaveEditFullname"
|
||||
></fullname-dialog>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialogEditAliasShown" max-width="600px">
|
||||
<alias-dialog
|
||||
:alias="userProfileStore.user.alias"
|
||||
@close="handleCloseEditAlias"
|
||||
@save="handleSaveEditAlias"
|
||||
></alias-dialog>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialogEditBirthdayShown" max-width="600px">
|
||||
<birthday-dialog
|
||||
:birth-date="userProfileStore.user.birthDate"
|
||||
@close="handleCloseEditBirthday"
|
||||
@save="handleSaveEditBirthday"
|
||||
></birthday-dialog>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialogEditPhoneShown" max-width="600px">
|
||||
<phone-dialog
|
||||
:phone="userProfileStore.user.phoneNumber"
|
||||
@close="handleCloseEditPhone"
|
||||
@save="handleSaveEditPhone"
|
||||
></phone-dialog>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialogEditEmailShown" max-width="600px">
|
||||
<email-dialog
|
||||
:email="userProfileStore.user.email"
|
||||
@close="handleCloseEditEmail"
|
||||
@save="handleSaveEditEmail"
|
||||
></email-dialog>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="dialogEditAddressShown" max-width="600px">
|
||||
<address-dialog
|
||||
:address="userProfileStore.user.address"
|
||||
@close="handleCloseEditAddress"
|
||||
@save="handleSaveEditAddress"
|
||||
></address-dialog>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import AddressDialog from './AddressDialog.vue';
|
||||
import EmailDialog from "./EmailDialog.vue";
|
||||
import PhoneDialog from "@/views/profile/account/PhoneDialog.vue";
|
||||
import BirthdayDialog from "@/views/profile/account/BirthdayDialog.vue";
|
||||
import AliasDialog from "@/views/profile/account/AliasDialog.vue";
|
||||
import FullnameDialog from "@/views/profile/account/FullnameDialog.vue";
|
||||
import PortraitDialog from "@/views/profile/account/PortraitDialog.vue";
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
|
||||
const userProfileStore = useUserProfileStore()
|
||||
|
||||
|
||||
// ### Portrait
|
||||
|
||||
const dialogEditPortraitShown = ref(false)
|
||||
|
||||
function openEditPortrait() {
|
||||
dialogEditPortraitShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditPortrait() {
|
||||
dialogEditPortraitShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditPortrait(portraitData) {
|
||||
userProfileStore.changePortrait(portraitData)
|
||||
dialogEditPortraitShown.value = false
|
||||
}
|
||||
|
||||
// ### Fullname
|
||||
|
||||
const dialogEditFullnameShown = ref(false)
|
||||
|
||||
function openEditFullname() {
|
||||
dialogEditFullnameShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditFullname() {
|
||||
dialogEditFullnameShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditFullname(firstname, lastname) {
|
||||
userProfileStore.changeFullname(firstname, lastname)
|
||||
dialogEditFullnameShown.value = false
|
||||
}
|
||||
|
||||
// ### Alias
|
||||
|
||||
const dialogEditAliasShown = ref(false)
|
||||
|
||||
function openEditAlias() {
|
||||
dialogEditAliasShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditAlias() {
|
||||
dialogEditAliasShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditAlias(alias) {
|
||||
userProfileStore.changeAlias(alias)
|
||||
dialogEditAliasShown.value = false
|
||||
}
|
||||
|
||||
// ### Birthday
|
||||
|
||||
const dialogEditBirthdayShown = ref(false)
|
||||
|
||||
function openEditBirthday() {
|
||||
dialogEditBirthdayShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditBirthday() {
|
||||
dialogEditBirthdayShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditBirthday(birthday) {
|
||||
userProfileStore.changeBirthday(birthday)
|
||||
dialogEditBirthdayShown.value = false
|
||||
}
|
||||
|
||||
// ### Phone
|
||||
|
||||
const dialogEditPhoneShown = ref(false)
|
||||
|
||||
function openEditPhone() {
|
||||
dialogEditPhoneShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditPhone() {
|
||||
dialogEditPhoneShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditPhone(phone) {
|
||||
userProfileStore.changePhone(phone)
|
||||
dialogEditPhoneShown.value = false
|
||||
}
|
||||
|
||||
// ### Email
|
||||
|
||||
const dialogEditEmailShown = ref(false)
|
||||
|
||||
function openEditEmail() {
|
||||
dialogEditEmailShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditEmail() {
|
||||
dialogEditEmailShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditEmail(email) {
|
||||
userProfileStore.changeEmail(email)
|
||||
dialogEditEmailShown.value = false
|
||||
}
|
||||
|
||||
// ### ADDRESS
|
||||
const dialogEditAddressShown = ref(false)
|
||||
|
||||
function openEditAddress() {
|
||||
dialogEditAddressShown.value = true
|
||||
}
|
||||
|
||||
function handleCloseEditAddress() {
|
||||
dialogEditAddressShown.value = false
|
||||
}
|
||||
|
||||
function handleSaveEditAddress(address) {
|
||||
userProfileStore.changeAddress(address)
|
||||
dialogEditAddressShown.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.editableValue {
|
||||
@apply py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full;
|
||||
@apply hover:bg-[#A6147D] hover:text-white hover:opacity-90;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply p-2 min-w-40 text-left;
|
||||
}
|
||||
|
||||
.value {
|
||||
@apply flex-auto pr-6 text-left;
|
||||
}
|
||||
</style>
|
||||
36
frontend/src/views/profile/account/AddressDialog.vue
Normal file
36
frontend/src/views/profile/account/AddressDialog.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Adresse
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="address"
|
||||
label="Votre adresse"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['address'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const address = ref(props.address);
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', address.value)
|
||||
</script>
|
||||
38
frontend/src/views/profile/account/AliasDialog.vue
Normal file
38
frontend/src/views/profile/account/AliasDialog.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ $t('personnalinformation.alias') }}
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="alias"
|
||||
:label="$t('personnalinformation.alias')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['alias'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const alias = ref(props.alias)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', alias.value)
|
||||
</script>
|
||||
|
||||
|
||||
36
frontend/src/views/profile/account/BirthdayDialog.vue
Normal file
36
frontend/src/views/profile/account/BirthdayDialog.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Date de naissance
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="birthDate"
|
||||
label="AAAA-MM-JJ"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['birthDate'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const birthDate = ref(props.birthDate)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', birthDate.value)
|
||||
</script>
|
||||
38
frontend/src/views/profile/account/EmailDialog.vue
Normal file
38
frontend/src/views/profile/account/EmailDialog.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Courriel
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="email"
|
||||
label="Votre courriel"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['email'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const email = ref(props.email)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', email.value)
|
||||
</script>
|
||||
|
||||
|
||||
45
frontend/src/views/profile/account/FullnameDialog.vue
Normal file
45
frontend/src/views/profile/account/FullnameDialog.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ $t('personnalinformation.fullname') }}
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="firstname"
|
||||
:label="$t('personnalinformation.firstname')"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="lastname"
|
||||
:label="$t('personnalinformation.lastname')"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['firstname', 'lastname'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const firstname = ref(props.firstname)
|
||||
const lastname = ref(props.lastname)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', firstname.value, lastname.value)
|
||||
</script>
|
||||
|
||||
|
||||
37
frontend/src/views/profile/account/PhoneDialog.vue
Normal file
37
frontend/src/views/profile/account/PhoneDialog.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Numéro de téléphone
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="phone"
|
||||
label="Votre numéro de téléphone"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['phone'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const phone = ref(props.phone)
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', phone.value)
|
||||
</script>
|
||||
58
frontend/src/views/profile/account/PortraitDialog.vue
Normal file
58
frontend/src/views/profile/account/PortraitDialog.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Portrait
|
||||
</v-card-title>
|
||||
|
||||
<div class="m-4">
|
||||
<img
|
||||
:src="portraitData"
|
||||
class="mb-5 w-full transition duration-200 ease-in-out transform"
|
||||
alt="Aperçu de la bannière"
|
||||
/>
|
||||
|
||||
<v-file-input
|
||||
@change="onSelectedFileChanged"
|
||||
v-model="selectedFile"
|
||||
variant="outlined"
|
||||
accept="image/*"
|
||||
label="Votre bannière"
|
||||
></v-file-input>
|
||||
</div>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn variant="plain" @click="requestClose">
|
||||
Annuler
|
||||
</v-btn>
|
||||
<v-btn color="#A6147D" @click="requestSave">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const props = defineProps(['portraitUrl'])
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
const portraitData = ref(props.portraitUrl)
|
||||
|
||||
const selectedFile = ref({})
|
||||
const onSelectedFileChanged = () => {
|
||||
if (selectedFile.value) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
portraitData.value = event.target.result
|
||||
}
|
||||
reader.readAsDataURL(selectedFile.value)
|
||||
} else {
|
||||
portraitData.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const requestClose = () => emit('close')
|
||||
const requestSave = () => emit('save', selectedFile.value)
|
||||
</script>
|
||||
75
frontend/src/views/profile/creators/BannerPicker.vue
Normal file
75
frontend/src/views/profile/creators/BannerPicker.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<h2 class="text-2xl font-semibold mb-4 flex justify-center">
|
||||
Bannière
|
||||
</h2>
|
||||
|
||||
<img
|
||||
:src="fileUrl || fallbackUrl"
|
||||
class="mb-5 w-full transition duration-200 ease-in-out transform"
|
||||
alt="Aperçu de la bannière"
|
||||
>
|
||||
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
variant="outlined"
|
||||
accept="image/*"
|
||||
label="Votre bannière"
|
||||
@change="onFileSelected"
|
||||
></v-file-input>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" @click="publish">Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useClient } from '@/plugins/api.js'
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['closeRequested'])
|
||||
|
||||
const selectedFile = ref({})
|
||||
const fileUrl = ref(props.creator.images.banner)
|
||||
const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png'
|
||||
|
||||
const onFileSelected = () => {
|
||||
if (selectedFile.value) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
fileUrl.value = event.target.result
|
||||
}
|
||||
reader.readAsDataURL(selectedFile.value)
|
||||
} else {
|
||||
fileUrl.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const client = useClient()
|
||||
const publish = async () => {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', selectedFile.value)
|
||||
|
||||
await client.post(
|
||||
`/api/creators/${props.creator.id}/banner`,
|
||||
formData
|
||||
)
|
||||
|
||||
props.creator.images.banner = fileUrl
|
||||
emits('closeRequested')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested')
|
||||
}
|
||||
</script>
|
||||
59
frontend/src/views/profile/creators/ChangeStripeID.vue
Normal file
59
frontend/src/views/profile/creators/ChangeStripeID.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['closeRequested']);
|
||||
|
||||
const stripeId = ref(props.creator.stripeId);
|
||||
|
||||
const client = useClient();
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await client.post(`/api/membership/stripe-account`, {
|
||||
stripeAccountId: stripeId.value,
|
||||
});
|
||||
|
||||
props.creator.stripeId = stripeId.value;
|
||||
emits('closeRequested');
|
||||
} catch (error) {
|
||||
console.error('Error saving stripe id:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Modifier le id Stripe</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="stripeId"
|
||||
label="Stripe Id"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" @click="save">Enregistrer</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
62
frontend/src/views/profile/creators/ChangeTitle.vue
Normal file
62
frontend/src/views/profile/creators/ChangeTitle.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['closeRequested']);
|
||||
|
||||
const title = ref(props.creator.title);
|
||||
|
||||
const client = useClient();
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await client.post(
|
||||
`/api/creators/${props.creator.id}/title`,
|
||||
{
|
||||
title: title.value
|
||||
}
|
||||
);
|
||||
|
||||
props.creator.title = title.value;
|
||||
emits('closeRequested');
|
||||
} catch (error) {
|
||||
console.error('Error saving title:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Modifier le Titre</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="title"
|
||||
label="Titre"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" @click="save">Enregistrer</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
250
frontend/src/views/profile/creators/ColorsPicker.vue
Normal file
250
frontend/src/views/profile/creators/ColorsPicker.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<script setup>
|
||||
import {ref, computed} from 'vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['closeRequested']);
|
||||
|
||||
const primaryColor = ref(props.creator.colors.primary)
|
||||
const secondaryColor = ref(props.creator.colors.secondary)
|
||||
const backgroundColor = ref(props.creator.colors.background)
|
||||
const errorColor = ref(props.creator.colors.error)
|
||||
const surfaceColor = ref(props.creator.colors.surface)
|
||||
const onPrimaryColor = ref(props.creator.colors.onPrimary)
|
||||
const onSecondaryColor = ref(props.creator.colors.onSecondary)
|
||||
const onBackgroundColor = ref(props.creator.colors.onBackground)
|
||||
const onErrorColor = ref(props.creator.colors.onError)
|
||||
const onSurfaceColor = ref(props.creator.colors.onSurface)
|
||||
|
||||
const selectedColorName = ref('primary');
|
||||
const selectedBackgroundColor = computed({
|
||||
get() {
|
||||
switch (selectedColorName.value) {
|
||||
case 'primary':
|
||||
return primaryColor.value
|
||||
case 'secondary':
|
||||
return secondaryColor.value
|
||||
case 'background':
|
||||
return backgroundColor.value
|
||||
case 'error':
|
||||
return errorColor.value
|
||||
case 'surface':
|
||||
return surfaceColor.value
|
||||
default:
|
||||
return '#e42aad';
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
switch (selectedColorName.value) {
|
||||
case 'primary':
|
||||
primaryColor.value = value
|
||||
break
|
||||
case 'secondary':
|
||||
secondaryColor.value = value
|
||||
break
|
||||
case 'background':
|
||||
backgroundColor.value = value
|
||||
break
|
||||
case 'error':
|
||||
errorColor.value = value
|
||||
break
|
||||
case 'surface':
|
||||
surfaceColor.value = value
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
const selectedTextColor = computed({
|
||||
get() {
|
||||
switch (selectedColorName.value) {
|
||||
case 'primary':
|
||||
return onPrimaryColor.value
|
||||
case 'secondary':
|
||||
return onSecondaryColor.value
|
||||
case 'background':
|
||||
return onBackgroundColor.value
|
||||
case 'error':
|
||||
return onErrorColor.value
|
||||
case 'surface':
|
||||
return onSurfaceColor.value
|
||||
default:
|
||||
return '#e42aad';
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
switch (selectedColorName.value) {
|
||||
case 'primary':
|
||||
onPrimaryColor.value = value
|
||||
break
|
||||
case 'secondary':
|
||||
onSecondaryColor.value = value
|
||||
break
|
||||
case 'background':
|
||||
onBackgroundColor.value = value
|
||||
break
|
||||
case 'error':
|
||||
onErrorColor.value = value
|
||||
break
|
||||
case 'surface':
|
||||
onSurfaceColor.value = value
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const selectColor = (colorName) => {
|
||||
selectedColorName.value = colorName;
|
||||
};
|
||||
|
||||
const client = useClient();
|
||||
const save = async () => {
|
||||
try {
|
||||
await client.post(
|
||||
`/api/creators/${props.creator.id}/colors`,
|
||||
{
|
||||
'primary': primaryColor.value,
|
||||
'secondary': secondaryColor.value,
|
||||
'background': backgroundColor.value,
|
||||
'error': errorColor.value,
|
||||
'surface': surfaceColor.value,
|
||||
'onPrimary': onPrimaryColor.value,
|
||||
'onSecondary': onSecondaryColor.value,
|
||||
'onBackground': onBackgroundColor.value,
|
||||
'onError': onErrorColor.value,
|
||||
'onSurface': onSurfaceColor.value,
|
||||
});
|
||||
|
||||
props.creator.colors.primary = primaryColor.value
|
||||
props.creator.colors.secondary = secondaryColor.value
|
||||
props.creator.colors.background = backgroundColor.value
|
||||
props.creator.colors.error = errorColor.value
|
||||
props.creator.colors.surface = surfaceColor.value
|
||||
props.creator.colors.onPrimary = onPrimaryColor.value
|
||||
props.creator.colors.onSecondary = onSecondaryColor.value
|
||||
props.creator.colors.onBackground = onBackgroundColor.value
|
||||
props.creator.colors.onError = onErrorColor.value
|
||||
props.creator.colors.onSurface = onSurfaceColor.value
|
||||
|
||||
emits('closeRequested');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-2xl font-semibold mb-4 flex justify-center">
|
||||
Palette de Couleurs
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-column gap-6 justify-center items-start mt-5">
|
||||
|
||||
<div class="grid grid-cols-5 grid-rows-1 gap-4">
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'primary' }"
|
||||
:style="{ backgroundColor: primaryColor }"
|
||||
@click="selectColor('primary')"
|
||||
>
|
||||
<span class="color-name"
|
||||
:style="{ color: onPrimaryColor }">
|
||||
Primary
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'secondary' }"
|
||||
:style="{ backgroundColor: secondaryColor }"
|
||||
@click="selectColor('secondary')"
|
||||
>
|
||||
<span class="color-name"
|
||||
:style="{ color: onSecondaryColor }">
|
||||
Secondary
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'surface' }"
|
||||
:style="{ backgroundColor: surfaceColor }"
|
||||
@click="selectColor('surface')"
|
||||
>
|
||||
<span class="color-name"
|
||||
:style="{ color: onSurfaceColor }">
|
||||
Surface
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'background' }"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
@click="selectColor('background')"
|
||||
>
|
||||
<span class="color-name"
|
||||
:style="{ color: onBackgroundColor }">
|
||||
Background
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'error' }"
|
||||
:style="{ backgroundColor: errorColor }"
|
||||
@click="selectColor('error')"
|
||||
>
|
||||
<span class="color-name"
|
||||
:style="{ color: onErrorColor }"
|
||||
>
|
||||
Error
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex row justify-center space-x-12 mx-auto">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="mb-2 font-weight-bold">Text</span>
|
||||
<v-color-picker v-model="selectedTextColor"></v-color-picker>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="mb-2 font-weight-bold">Background</span>
|
||||
<v-color-picker v-model="selectedBackgroundColor"></v-color-picker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4 mt-4">
|
||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" @click="save">Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-square {
|
||||
@apply w-[150px] h-[150px];
|
||||
@apply flex rounded-md cursor-pointer relative;
|
||||
@apply items-center justify-center;
|
||||
@apply font-bold text-2xl;
|
||||
}
|
||||
|
||||
.color-square.selected {
|
||||
@apply border-4 border-solid border-[crimson];
|
||||
}
|
||||
|
||||
.color-name {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
84
frontend/src/views/profile/creators/CreateCreator.vue
Normal file
84
frontend/src/views/profile/creators/CreateCreator.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const creatorName = ref('');
|
||||
const errorMessage = ref('');
|
||||
const isLoading = ref(false);
|
||||
|
||||
const router = useRouter();
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const userProfileStore = useUserProfileStore();
|
||||
|
||||
async function createAccount() {
|
||||
const client = useClient();
|
||||
try {
|
||||
errorMessage.value = '';
|
||||
isLoading.value = true;
|
||||
const normalizedCreatorName = creatorName.value.toLowerCase();
|
||||
await client.post('/api/creators', {
|
||||
creatorId: userProfileStore.user.id,
|
||||
name: normalizedCreatorName,
|
||||
});
|
||||
await creatorProfileStore.fetchCurrentCreatorProfile();
|
||||
await router.push(`/@${normalizedCreatorName}`);
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.errors) {
|
||||
errorMessage.value = error.response.data.errors[0]?.['reason'] || 'An unexpected error occurred.';
|
||||
} else {
|
||||
errorMessage.value = error?.response?.data?.message || error.message || 'An unexpected error occurred.';
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="create-creator-card">
|
||||
<div class="py-2 text-3xl font-bold">
|
||||
<div class="text-center mb-10">Créez votre Hutopy.</div>
|
||||
</div>
|
||||
<div class="flex flex-column justify-end gap-2">
|
||||
<v-alert
|
||||
v-if="!!errorMessage"
|
||||
dense
|
||||
outlined
|
||||
text
|
||||
type="error"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="creatorName"
|
||||
label="Nom de la page"
|
||||
outlined
|
||||
></v-text-field>
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
|
||||
<v-btn
|
||||
:disabled="isLoading"
|
||||
variant="outlined"
|
||||
@click="createAccount"
|
||||
:style="{ borderColor: 'rgb(159, 76, 173)', color: 'rgb(159, 76, 173)' }"
|
||||
>
|
||||
Créer
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.create-creator-card {
|
||||
@apply text-center max-w-[1000px] mx-auto p-10 bg-white shadow-2xl rounded mt-16 ;
|
||||
}
|
||||
|
||||
</style>
|
||||
312
frontend/src/views/profile/creators/CreatorPage.vue
Normal file
312
frontend/src/views/profile/creators/CreatorPage.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<script setup>
|
||||
import XIcon from '@/assets/icons/x.svg';
|
||||
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
||||
import ChangeStripeID from '@/views/profile/creators/ChangeStripeID.vue';
|
||||
import ChangeTitle from '@/views/profile/creators/ChangeTitle.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import BannerPicker from './BannerPicker.vue';
|
||||
import ColorsPicker from './ColorsPicker.vue';
|
||||
import LogoPicker from './LogoPicker.vue';
|
||||
import Socials from './Socials.vue';
|
||||
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
console.log(creatorProfileStore.creator);
|
||||
const imageBanner = computed(
|
||||
() =>
|
||||
creatorProfileStore.creator.images.banner ||
|
||||
'/images/placeholders/banner.png'
|
||||
);
|
||||
const imageLogo = computed(
|
||||
() =>
|
||||
creatorProfileStore.creator.images.logo || '/images/placeholders/logo.png'
|
||||
);
|
||||
|
||||
const dialog = ref(false);
|
||||
const currentComponent = ref('');
|
||||
|
||||
const componentsMap = {
|
||||
BannerPicker,
|
||||
LogoPicker,
|
||||
Socials,
|
||||
ColorsPicker,
|
||||
ChangeTitle,
|
||||
ChangeStripeID,
|
||||
};
|
||||
|
||||
|
||||
function requestCancel() {
|
||||
currentComponent.value = null;
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
const openDialog = (component) => {
|
||||
currentComponent.value = componentsMap[component];
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
currentComponent.value = null;
|
||||
dialog.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="800px">
|
||||
<v-card
|
||||
:style="{ borderRadius: '25px', border: '3px solid rgb(159, 76, 173)' }"
|
||||
>
|
||||
<v-card-text>
|
||||
<component
|
||||
:is="currentComponent"
|
||||
:creator="creatorProfileStore.creator"
|
||||
@closeRequested="closeDialog"
|
||||
@requestAccept="requestAccept"
|
||||
@requestCancel="requestCancel"
|
||||
></component>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Lorsque l'utilisateur n'a pas de creator name-->
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<h1 class="uppercase pb-5 text-2xl">
|
||||
<v-icon class="mr-2">mdi-file-edit-outline</v-icon>
|
||||
{{ $t('creatorinfopage.pageinformation') }}
|
||||
</h1>
|
||||
|
||||
<div v-if="creatorProfileStore.hasCreator" class="w-full max-w-[800px]">
|
||||
<div class="my-10 border rounded bg-white">
|
||||
<div class="py-5 uppercase ml-4">
|
||||
{{ $t('creatorinfopage.informations') }}
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<button
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="flex-none pa-2 min-w-32 text-left">{{
|
||||
$t('creatorinfopage.name')
|
||||
}}</span>
|
||||
<span class="flex-auto text-left pr-6 capitalize">{{
|
||||
creatorProfileStore.creator.name
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<button
|
||||
@click="openDialog('ChangeTitle')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="flex-none pa-2 min-w-32 text-left">{{
|
||||
$t('creatorinfopage.title')
|
||||
}}</span>
|
||||
<span class="flex-auto text-left pr-6 capitalize">{{
|
||||
creatorProfileStore.creator.title
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<button
|
||||
@click="openDialog('ChangeStripeID')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full rounded-b"
|
||||
>
|
||||
<span class="flex-none pa-2 min-w-32 text-left"
|
||||
>Stripe Account ID</span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.stripeId
|
||||
}}</span>
|
||||
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border rounded bg-white">
|
||||
<div class="py-5 uppercase ml-4">
|
||||
{{ $t('creatorinfopage.banner&profile') }}
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<button
|
||||
@click="openDialog('ColorsPicker')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="flex-auto text-left pr-6 capitalize">
|
||||
Choisissez votre palette de couleurs.
|
||||
</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button>
|
||||
<img
|
||||
@click="openDialog('BannerPicker')"
|
||||
:src="imageBanner"
|
||||
class="w-full transition duration-200 ease-in-out transform hover:brightness-125"
|
||||
alt="Tutorial Banner"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button class="flex justify-center my-5">
|
||||
<img
|
||||
@click="openDialog('LogoPicker')"
|
||||
class="custom-border hover:brightness-125 active:bg-gray-600 shadow flex items-center transition duration-200 ease-in-out w-48 h-48 rounded-full"
|
||||
:src="imageLogo"
|
||||
alt="Profile Image"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 border rounded bg-white">
|
||||
<div class="py-5 uppercase ml-4">
|
||||
{{ $t('creatorinfopage.socialnetwork') }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full ">
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="pa-2 min-w-32 text-left"
|
||||
><v-icon>mdi-facebook</v-icon></span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.facebookUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="flex-none pa-2 min-w-32 text-left">
|
||||
<v-icon>mdi-instagram</v-icon></span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.instagramUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="flex-none pa-2 w-9 h-9 text-left ml-0.5">
|
||||
<XIcon></XIcon>
|
||||
</span>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.xUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="pa-2 min-w-32 text-left"
|
||||
><v-icon>mdi-linkedin</v-icon></span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.linkedInUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="flex-none pa-2 min-w-32 text-left">
|
||||
<XIcon class="w-5 h-5"></XIcon>
|
||||
</span>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.tikTokUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="pa-2 min-w-32 text-left"
|
||||
><v-icon>mdi-youtube</v-icon></span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.youtubeUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||
>
|
||||
<span class="pa-2 min-w-32 text-left"
|
||||
><v-icon>mdi-reddit</v-icon></span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.redditUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('Socials')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full rounded-b"
|
||||
>
|
||||
<span class="pa-2 min-w-32 text-left"
|
||||
><v-icon>mdi-web</v-icon></span
|
||||
>
|
||||
<span class="flex-auto text-left pr-6">{{
|
||||
creatorProfileStore.creator.socials.websiteUrl
|
||||
}}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.HoverBtn:hover {
|
||||
@apply bg-[#A6147D] text-white;
|
||||
@apply hover:opacity-90;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border: 3px solid;
|
||||
}
|
||||
</style>
|
||||
78
frontend/src/views/profile/creators/LogoPicker.vue
Normal file
78
frontend/src/views/profile/creators/LogoPicker.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<h2 class="text-2xl font-semibold mb-4 flex justify-center">
|
||||
Logo
|
||||
</h2>
|
||||
|
||||
<div class="flex justify-center mb-5">
|
||||
<img
|
||||
:src="fileUrl || fallbackUrl"
|
||||
class="w-full transition duration-200 ease-in-out transform max-w-[400px]"
|
||||
alt="Aperçu du logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
variant="outlined"
|
||||
accept="image/*"
|
||||
label="Votre logo"
|
||||
@change="onFileSelected"
|
||||
></v-file-input>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" @click="publish">Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useClient } from '@/plugins/api.js'
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['closeRequested'])
|
||||
|
||||
const selectedFile = ref("")
|
||||
const fileUrl = ref(props.creator.images.logo)
|
||||
const fallbackUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png' // Chemin de votre image de secours
|
||||
|
||||
const onFileSelected = () => {
|
||||
if (selectedFile.value) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
fileUrl.value = event.target.result
|
||||
}
|
||||
reader.readAsDataURL(selectedFile.value)
|
||||
} else {
|
||||
fileUrl.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const client = useClient()
|
||||
const publish = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile.value)
|
||||
|
||||
await client.post(
|
||||
`/api/creators/${props.creator.id}/logo`,
|
||||
formData)
|
||||
|
||||
props.creator.images.logo = fileUrl.value;
|
||||
emits('closeRequested');
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested')
|
||||
}
|
||||
</script>
|
||||
|
||||
161
frontend/src/views/profile/creators/Socials.vue
Normal file
161
frontend/src/views/profile/creators/Socials.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup>
|
||||
import XIcon from '@/assets/icons/x.svg'
|
||||
import {ref} from 'vue'
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['closeRequested'])
|
||||
|
||||
const facebookUrl = ref(props.creator.socials.facebookUrl)
|
||||
const instagramUrl = ref(props.creator.socials.instagramUrl)
|
||||
const linkedInUrl = ref(props.creator.socials.linkedInUrl)
|
||||
const redditUrl = ref(props.creator.socials.redditUrl)
|
||||
const tikTokUrl = ref(props.creator.socials.tikTokUrl)
|
||||
const websiteUrl = ref(props.creator.socials.websiteUrl)
|
||||
const xUrl = ref(props.creator.socials.xUrl)
|
||||
const youtubeUrl = ref(props.creator.socials.youtubeUrl)
|
||||
|
||||
const client = useClient()
|
||||
const save = async () => {
|
||||
try {
|
||||
await client.post(
|
||||
`/api/creators/${props.creator.id}/socials`,
|
||||
{
|
||||
"facebookUrl": facebookUrl.value || null,
|
||||
"instagramUrl": instagramUrl.value || null,
|
||||
"linkedInUrl": linkedInUrl.value || null,
|
||||
"redditUrl": redditUrl.value || null,
|
||||
"tikTokUrl": tikTokUrl.value || null,
|
||||
"websiteUrl": websiteUrl.value || null,
|
||||
"xUrl": xUrl.value || null,
|
||||
"youtubeUrl": youtubeUrl.value || null,
|
||||
})
|
||||
|
||||
props.creator.socials.facebookUrl = facebookUrl
|
||||
props.creator.socials.instagramUrl = instagramUrl
|
||||
props.creator.socials.linkedInUrl = linkedInUrl
|
||||
props.creator.socials.redditUrl = redditUrl
|
||||
props.creator.socials.tikTokUrl = tikTokUrl
|
||||
props.creator.socials.websiteUrl = websiteUrl
|
||||
props.creator.socials.xUrl = xUrl
|
||||
props.creator.socials.youtubeUrl = youtubeUrl
|
||||
|
||||
emits('closeRequested')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
|
||||
width: 28px;
|
||||
height: 30px;
|
||||
fill: #000000;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Reseaux Sociaux</div>
|
||||
<div class="flex flex-row align-center">
|
||||
<v-icon class="mb-5 mr-2">mdi-facebook</v-icon>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="facebookUrl"
|
||||
label="Lien Facebook"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-icon class="mb-5 mr-2">mdi-instagram</v-icon>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="instagramUrl"
|
||||
label="Lien Instagram"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-icon class="mb-5 mr-2">mdi-linkedin</v-icon>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="linkedInUrl"
|
||||
label="Lien LinkedIn"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-icon class="mb-5 mr-2">mdi-reddit</v-icon>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="redditUrl"
|
||||
label="Lien Reddit"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<div class="w-6 h-6 mb-5 mr-2">
|
||||
<XIcon></XIcon>
|
||||
</div>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="tikTokUrl"
|
||||
label="Lien TikTok"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-icon class="mb-5 mr-2">mdi-web</v-icon>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="websiteUrl"
|
||||
label="Lien site web"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
<div class="flex flex-row align-center">
|
||||
|
||||
<div class="w-6 h-6 mb-5 mr-2">
|
||||
<XIcon class="icon"></XIcon>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="xUrl"
|
||||
label="Lien X"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-icon class="mb-5 mr-2">mdi-youtube</v-icon>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="youtubeUrl"
|
||||
label="Lien Youtube"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" @click="save">Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
25
frontend/src/views/svg/HutopySvg.vue
Normal file
25
frontend/src/views/svg/HutopySvg.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<div>
|
||||
<svg width="45" height="45" viewBox="0 0 540 540" :style="{ color: branding.colors.onPrimary }">
|
||||
<path fill="currentColor" d="M0 0 C2.69849689 0.0031451 5.39587704 -0.02033719 8.09423828 -0.0456543 C24.64095924 -0.10755047 40.6877731 1.83145667 56.59814453 6.50317383 C57.64365479 6.80505615 58.68916504 7.10693848 59.76635742 7.41796875 C92.79656028 17.21576522 120.14877162 34.84217931 145.09814453 58.31567383 C145.80712891 58.97051758 146.51611328 59.62536133 147.24658203 60.30004883 C156.43027452 69.07141226 164.10800524 79.75645166 171.09814453 90.31567383 C171.99726562 91.64985352 171.99726562 91.64985352 172.91455078 93.01098633 C202.24624509 138.40104257 210.21713133 193.46657021 199.45556641 245.92919922 C192.90962056 275.68852135 179.08829353 305.20779517 159.09814453 328.31567383 C158.12705075 329.46844293 157.15708766 330.62216508 156.18798828 331.77661133 C145.74534244 344.15953661 145.74534244 344.15953661 140.09814453 349.31567383 C139.43814453 349.31567383 138.77814453 349.31567383 138.09814453 349.31567383 C138.09814453 349.97567383 138.09814453 350.63567383 138.09814453 351.31567383 C125.46428062 363.48565937 109.61785837 373.30307857 94.09814453 381.31567383 C93.49325195 381.62843262 92.88835937 381.94119141 92.26513672 382.26342773 C64.72703152 396.27155233 34.49385055 402.76232703 3.72314453 402.69067383 C2.84274506 402.69013 1.96234558 402.68958618 1.05526733 402.68902588 C-13.25349056 402.64848426 -26.95433873 401.73425935 -40.90185547 398.31567383 C-42.23821045 397.99421387 -42.23821045 397.99421387 -43.6015625 397.66625977 C-72.07884569 390.61747809 -98.87807183 378.00655878 -121.53857422 359.26879883 C-124.98230298 356.422742 -128.53024689 353.7103623 -132.06591797 350.97973633 C-132.97470703 350.15602539 -132.97470703 350.15602539 -133.90185547 349.31567383 C-133.90185547 348.65567383 -133.90185547 347.99567383 -133.90185547 347.31567383 C-134.56185547 347.31567383 -135.22185547 347.31567383 -135.90185547 347.31567383 C-135.90185547 346.65567383 -135.90185547 345.99567383 -135.90185547 345.31567383 C-136.56185547 345.31567383 -137.22185547 345.31567383 -137.90185547 345.31567383 C-139.24169922 343.98754883 -139.24169922 343.98754883 -140.83935547 342.06567383 C-143.10188476 339.38611053 -145.39815773 336.76489417 -147.80810547 334.21801758 C-148.30834229 333.68660156 -148.8085791 333.15518555 -149.32397461 332.60766602 C-150.32555716 331.54718583 -151.33248849 330.49172924 -152.3449707 329.44165039 C-155.90185547 325.64913211 -155.90185547 325.64913211 -155.90185547 322.31567383 C-151.7526342 322.86293323 -149.09751951 325.32327358 -145.96435547 327.87817383 C-112.49996321 354.28872765 -70.28107399 364.92725762 -28.16088867 360.37231445 C11.7404784 355.37903429 50.51367105 333.83116901 75.28564453 302.00317383 C102.2073581 266.1942078 113.74287629 223.77918143 108.03564453 179.19067383 C103.36182966 146.79964046 88.57557441 116.98148746 66.09814453 93.31567383 C65.60604492 92.78570801 65.11394531 92.25574219 64.60693359 91.7097168 C40.8738918 66.25446714 8.58347239 50.05740641 -25.90185547 45.31567383 C-27.07103516 45.13391602 -28.24021484 44.9521582 -29.44482422 44.76489258 C-71.61533582 39.65438979 -112.89208728 51.7135382 -146.31201172 77.46020508 C-149.8276055 80.21952147 -153.19809546 83.10839876 -156.42138672 86.20629883 C-157.90185547 87.31567383 -157.90185547 87.31567383 -160.90185547 87.31567383 C-160.35570146 83.08040407 -157.80081795 80.22554824 -155.21435547 77.00317383 C-154.72813721 76.3936084 -154.24191895 75.78404297 -153.7409668 75.15600586 C-143.89916607 62.99985508 -133.13277345 52.06245136 -120.90185547 42.31567383 C-120.33450684 41.85741211 -119.7671582 41.39915039 -119.18261719 40.92700195 C-97.55281967 23.63297615 -71.10136924 11.56026197 -44.27685547 5.06567383 C-43.26478027 4.81954346 -42.25270508 4.57341309 -41.20996094 4.31982422 C-27.42871573 1.10789603 -14.10433559 -0.03420308 0 0 Z " transform="translate(258.90185546875,69.684326171875)"/>
|
||||
<path fill="currentColor" d="M0 0 C23.93418963 20.21356155 38.18828481 47.87411238 43.30859375 78.609375 C43.72796962 83.82104607 43.79621346 89.00908218 43.74609375 94.234375 C43.7423877 94.93147583 43.73868164 95.62857666 43.73486328 96.34680176 C43.51079954 126.08447361 33.84943539 152.98108038 14.30859375 175.609375 C13.37273437 176.74439453 13.37273437 176.74439453 12.41796875 177.90234375 C7.3119221 183.93204517 1.58692987 188.83219983 -4.69140625 193.609375 C-5.65691406 194.351875 -6.62242187 195.094375 -7.6171875 195.859375 C-33.76277727 214.99515608 -66.44544142 222.27976227 -98.37890625 217.421875 C-129.2224086 212.211016 -156.10496998 196.18588064 -175.99609375 172.10546875 C-177.69140625 169.609375 -177.69140625 169.609375 -177.69140625 166.609375 C-176.37140625 166.939375 -175.05140625 167.269375 -173.69140625 167.609375 C-173.69140625 168.269375 -173.69140625 168.929375 -173.69140625 169.609375 C-172.85222656 170.00576172 -172.85222656 170.00576172 -171.99609375 170.41015625 C-169.47801141 171.72041265 -167.18096648 173.22649078 -164.81640625 174.796875 C-141.95286732 189.35191305 -116.13355444 194.94004479 -89.48388672 189.12792969 C-74.00965166 185.43003002 -61.09388018 178.62819028 -48.69140625 168.609375 C-48.05976563 168.14273438 -47.428125 167.67609375 -46.77734375 167.1953125 C-29.95140734 154.19707727 -19.18769244 130.97695501 -16.03515625 110.39453125 C-15.865 109.01587891 -15.865 109.01587891 -15.69140625 107.609375 C-15.55734375 106.55621094 -15.42328125 105.50304687 -15.28515625 104.41796875 C-12.8813248 80.40697048 -19.85309354 55.11934523 -34.69921875 36.02734375 C-35.35664063 35.22941406 -36.0140625 34.43148437 -36.69140625 33.609375 C-37.32820312 32.82175781 -37.965 32.03414062 -38.62109375 31.22265625 C-54.38763523 12.57508008 -77.98681734 0.98006353 -102.19970703 -1.04858398 C-115.33840527 -1.9297872 -128.14870541 -0.56547477 -140.69140625 3.609375 C-141.71621094 3.94710937 -142.74101562 4.28484375 -143.796875 4.6328125 C-155.05737548 8.64192736 -164.81345042 14.73678833 -173.984375 22.359375 C-175.69140625 23.609375 -175.69140625 23.609375 -177.69140625 23.609375 C-176.02661937 12.90007435 -162.17010016 2.50403181 -153.91601562 -3.73193359 C-107.16913616 -37.54750108 -44.81974776 -36.84949238 0 0 Z " transform="translate(291.69140625,175.390625)"/>
|
||||
</svg>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useBrandingStore } from "@/stores/brandingStore.js";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const branding = useBrandingStore();
|
||||
|
||||
return {
|
||||
branding,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
72
frontend/src/views/tools/SizeIndicator.vue
Normal file
72
frontend/src/views/tools/SizeIndicator.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<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