Transit
This commit is contained in:
65
frontend/src/views/creators/ActualBanner.vue
Normal file
65
frontend/src/views/creators/ActualBanner.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
|
||||
<div class="relative">
|
||||
<!-- Banner Container with mouse events -->
|
||||
<div
|
||||
class="relative"
|
||||
@mouseenter="showTint = isCurrentCreator"
|
||||
@mouseleave="showTint = false"
|
||||
@click="isCurrentCreator && openBannerEditor()"
|
||||
>
|
||||
<img
|
||||
class="w-full drop-shadow-[0_10px_6px_rgba(0,0,0,0.25)] h-60"
|
||||
:src="brandingStore.value.images.banner ? brandingStore.value.images.banner : '/images/placeholders/banner.png'"
|
||||
alt="Profile Banner"
|
||||
>
|
||||
<!-- Tint Effect -->
|
||||
<div
|
||||
v-if="showTint"
|
||||
class="absolute inset-0 bg-black/25 cursor-pointer"
|
||||
>
|
||||
<!-- Top-right Icon -->
|
||||
<div
|
||||
class="absolute top-4 right-4 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<v-icon large>mdi-pencil</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||
<template #default="{ close }">
|
||||
<div class="bg-white rounded-2xl p-4">
|
||||
<banner-editor :creator="brandingStore.value"
|
||||
@closeRequested="() => isDialogOpen = false"
|
||||
></banner-editor>
|
||||
</div>
|
||||
</template>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BannerEditor from "@/views/creators/BannerEditor.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const brandingStore = useBrandingStore();
|
||||
|
||||
// State
|
||||
const showTint = ref(false);
|
||||
const isDialogOpen = ref(false);
|
||||
|
||||
// Methods
|
||||
const openBannerEditor = () => {
|
||||
isDialogOpen.value = true;
|
||||
};
|
||||
|
||||
const isCurrentCreator = computed(() => {
|
||||
return authStore.userId === brandingStore.value.id;
|
||||
});
|
||||
|
||||
</script>
|
||||
25
frontend/src/views/creators/Banner.vue
Normal file
25
frontend/src/views/creators/Banner.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<!-- PC -->
|
||||
<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>
|
||||
|
||||
<actual-banner></actual-banner>
|
||||
<banner-actions></banner-actions>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import ActualBanner from "@/views/creators/ActualBanner.vue";
|
||||
import BannerActions from "@/views/creators/BannerActions.vue";
|
||||
|
||||
const branding = useBrandingStore();
|
||||
</script>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
import { useBrandingStore } from '@/stores/brandingStore.js';
|
||||
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";
|
||||
import {onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import CreatorLogo from "@/views/creators/CreatorLogo.vue";
|
||||
import NameTitle from "@/views/creators/NameTitle.vue";
|
||||
|
||||
const brandingStore = useBrandingStore();
|
||||
const isMobile = ref(false);
|
||||
@@ -80,10 +81,10 @@ onMounted(async () => {
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isSticky.value = !entry.isIntersecting;
|
||||
},
|
||||
{ threshold: 0 }
|
||||
([entry]) => {
|
||||
isSticky.value = !entry.isIntersecting;
|
||||
},
|
||||
{threshold: 0}
|
||||
);
|
||||
|
||||
if (mainContainer.value) {
|
||||
@@ -109,117 +110,48 @@ onBeforeUnmount(() => {
|
||||
<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>
|
||||
<div class="rounded-b-2xl shadow-2xl"
|
||||
:style="{
|
||||
backgroundColor: brandingStore.colors.primary,
|
||||
boxShadow: '0 5px 10px rgba(0, 0, 0, 0.3)',
|
||||
}">
|
||||
|
||||
<div class="flex flex-row p-2">
|
||||
<!-- 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>
|
||||
<creator-logo/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<name-title></name-title>
|
||||
</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 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="{
|
||||
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 }"
|
||||
: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"
|
||||
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>
|
||||
@@ -227,8 +159,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 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="{
|
||||
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)',
|
||||
}"
|
||||
@@ -236,20 +168,20 @@ onBeforeUnmount(() => {
|
||||
<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-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"
|
||||
v-else
|
||||
:src="socialNetwork.icon"
|
||||
class="w-6 h-6 mt-0.5"
|
||||
:alt="socialNetwork.url"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
@@ -258,28 +190,3 @@ onBeforeUnmount(() => {
|
||||
</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>
|
||||
|
||||
75
frontend/src/views/creators/BannerEditor.vue
Normal file
75
frontend/src/views/creators/BannerEditor.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<h2 class="text-2xl font-semibold mb-4">
|
||||
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>
|
||||
93
frontend/src/views/creators/CreateCreator.vue
Normal file
93
frontend/src/views/creators/CreateCreator.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} 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";
|
||||
import NameEditor from "@/views/creators/NameEditor.vue";
|
||||
|
||||
const creatorName = ref('');
|
||||
const creatorNameReservationId = ref(undefined);
|
||||
const canSave = computed(() => creatorNameReservationId.value !== undefined)
|
||||
|
||||
const isOperationPending = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const router = useRouter();
|
||||
const creatorProfileStore = useCreatorProfileStore();
|
||||
const userProfileStore = useUserProfileStore();
|
||||
|
||||
function handleCreatorNameReservationIdChanged($event) {
|
||||
console.log(`in handleCreatorNameReservationIdChanged: ${$event.value}`);
|
||||
creatorNameReservationId.value = $event.value
|
||||
}
|
||||
|
||||
async function createAccount() {
|
||||
try {
|
||||
isOperationPending.value = true;
|
||||
const client = useClient();
|
||||
errorMessage.value = '';
|
||||
const normalizedCreatorName = creatorName.value.toLowerCase();
|
||||
await client.post('/api/creators', {
|
||||
creatorId: userProfileStore.user.id,
|
||||
name: normalizedCreatorName,
|
||||
slugReservationId: creatorNameReservationId.value,
|
||||
});
|
||||
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 {
|
||||
isOperationPending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="create-creator-card">
|
||||
|
||||
<div class="py-2 text-3xl font-bold text-center mb-10">
|
||||
Créez votre Hutopy.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column justify-end gap-2">
|
||||
<v-alert
|
||||
v-if="!!errorMessage"
|
||||
outlined
|
||||
type="error"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<name-editor
|
||||
v-model:name="creatorName"
|
||||
creator-name-reservation-id="creatorNameDirty"
|
||||
@update:creator-name-reservation-id="handleCreatorNameReservationIdChanged($event)"
|
||||
></name-editor>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<v-btn
|
||||
:disabled="!canSave"
|
||||
variant="outlined"
|
||||
@click="createAccount"
|
||||
:style="{ borderColor: 'rgb(159, 76, 173)', color: 'rgb(159, 76, 173)' }"
|
||||
>
|
||||
Créer
|
||||
</v-btn>
|
||||
</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>
|
||||
@@ -1,94 +0,0 @@
|
||||
<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>
|
||||
@@ -5,7 +5,7 @@
|
||||
<v-progress-linear indeterminate></v-progress-linear>
|
||||
</div>
|
||||
<div v-else>
|
||||
<creator-banner></creator-banner>
|
||||
<banner></banner>
|
||||
</div>
|
||||
<div class="py-8 flex-grow">
|
||||
<router-view></router-view>
|
||||
@@ -16,11 +16,11 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script async setup>
|
||||
import CreatorBanner from "@/views/creators/CreatorBanner.vue";
|
||||
import Banner from "@/views/creators/Banner.vue";
|
||||
import Footer from "@/views/main/Footer.vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
const brandingStore = useBrandingStore()
|
||||
|
||||
70
frontend/src/views/creators/CreatorLogo.vue
Normal file
70
frontend/src/views/creators/CreatorLogo.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="rounded-full relative bg-red"
|
||||
@mouseenter="showTint = isCurrentCreator"
|
||||
@mouseleave="showTint = false"
|
||||
@click="isCurrentCreator && openBannerEditor()"
|
||||
>
|
||||
|
||||
<img
|
||||
class="shadow-2xl rounded-full border-solid border-102 max-w-[190px]"
|
||||
:src="brandingStore.value.images.logo
|
||||
? brandingStore.value.images.logo
|
||||
: '/images/placeholders/logo.png'"
|
||||
alt="Profile Picture"
|
||||
:style="{
|
||||
borderColor: brandingStore.colors.secondary,
|
||||
height: '190px',
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- Tint Effect -->
|
||||
<div
|
||||
v-if="showTint"
|
||||
class="absolute rounded-full inset-0 bg-black/25 cursor-pointer"
|
||||
>
|
||||
<!-- Top-right Icon -->
|
||||
<div
|
||||
class="absolute top-4 right-4 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<v-icon large>mdi-pencil</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||
<template #default="{ close }">
|
||||
<div class="bg-white rounded-2xl p-4">
|
||||
<creator-logo-editor
|
||||
:creator="brandingStore?.value"
|
||||
@closeRequested="() => isDialogOpen = false"
|
||||
></creator-logo-editor>
|
||||
</div>
|
||||
</template>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import CreatorLogoEditor from "@/views/creators/CreatorLogoEditor.vue";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const brandingStore = useBrandingStore();
|
||||
|
||||
// State
|
||||
const showTint = ref(false);
|
||||
const isDialogOpen = ref(false);
|
||||
|
||||
// Methods
|
||||
const openBannerEditor = () => {
|
||||
isDialogOpen.value = true;
|
||||
};
|
||||
|
||||
const isCurrentCreator = computed(() => {
|
||||
return authStore.userId === brandingStore.value.id;
|
||||
});
|
||||
|
||||
</script>
|
||||
79
frontend/src/views/creators/CreatorLogoEditor.vue
Normal file
79
frontend/src/views/creators/CreatorLogoEditor.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<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>
|
||||
|
||||
89
frontend/src/views/creators/NameEditor.vue
Normal file
89
frontend/src/views/creators/NameEditor.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import {v7} from "uuid";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
required: true
|
||||
},
|
||||
creatorNameReservationId: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits([
|
||||
'update:name',
|
||||
'update:creatorNameReservationId'
|
||||
]);
|
||||
|
||||
const name = ref(props.name);
|
||||
const isReserved = computed(() => reservationState.value === 'reserved');
|
||||
|
||||
const isOperationPending = ref(false);
|
||||
const reservationState = ref(null);
|
||||
const reservationId = ref(null);
|
||||
|
||||
let timeout = null;
|
||||
const handleInput = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() =>
|
||||
checkNameAvailability(),
|
||||
200);
|
||||
};
|
||||
|
||||
const client = useClient()
|
||||
const checkNameAvailability = async () => {
|
||||
if (!name.value || name.value.trim() === "") {
|
||||
reservationState.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = v7();
|
||||
isOperationPending.value = true;
|
||||
reservationState.value = "loading";
|
||||
await client.post(
|
||||
`/api/creators/@${encodeURIComponent(name.value)}/reserve`,
|
||||
{reservationId: id}
|
||||
);
|
||||
reservationState.value = "reserved";
|
||||
reservationId.value = id;
|
||||
} catch (error) {
|
||||
reservationState.value = "unavailable"; // Handle API failure case
|
||||
reservationId.value = undefined;
|
||||
} finally {
|
||||
emits('update:name', name);
|
||||
emits('update:creatorNameReservationId', reservationId);
|
||||
isOperationPending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
label="Nom de la page"
|
||||
v-model="name"
|
||||
outlined
|
||||
@input="handleInput"
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-progress-circular
|
||||
v-if="reservationState === 'loading'"
|
||||
indeterminate
|
||||
size="24"
|
||||
width="3"
|
||||
color="grey"
|
||||
></v-progress-circular>
|
||||
|
||||
<v-icon v-else-if="reservationState === 'reserved'" color="green">mdi-check-circle</v-icon>
|
||||
<v-icon v-else-if="reservationState === 'unavailable'" color="red">mdi-close-circle</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
79
frontend/src/views/creators/NameTitle.vue
Normal file
79
frontend/src/views/creators/NameTitle.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
<div class="relative flex flex-row"
|
||||
@mouseenter="showTint = isCurrentCreator"
|
||||
@mouseleave="showTint = false"
|
||||
@click="isCurrentCreator && openBannerEditor()"
|
||||
>
|
||||
|
||||
<div v-show="brandingStore.value.verified"
|
||||
class="text-blue m-4">
|
||||
<icon-account-verified></icon-account-verified>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col"
|
||||
:style="{ color: brandingStore.colors.onPrimary }">
|
||||
<span class="capitalize text-3xl">
|
||||
{{ brandingStore.value.name }}
|
||||
</span>
|
||||
|
||||
<span class="capitalize text-lg">
|
||||
{{ brandingStore.value.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tint Effect -->
|
||||
<div
|
||||
v-if="showTint"
|
||||
class="absolute inset-0 bg-black/25 cursor-pointer"
|
||||
>
|
||||
<!-- Top-right Icon -->
|
||||
<div
|
||||
class="absolute top-1 right-1 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<v-icon large>mdi-pencil</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||
<template #default="{ close }">
|
||||
<div class="bg-white rounded-2xl p-4">
|
||||
<name-title-editor
|
||||
:creator="brandingStore?.value"
|
||||
@closeRequested="() => isDialogOpen = false"
|
||||
></name-title-editor>
|
||||
</div>
|
||||
</template>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IconAccountVerified from "@/components/icons/IconAccountVerified.vue";
|
||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {computed, ref} from "vue";
|
||||
import NameTitleEditor from "@/views/creators/NameTitleEditor.vue";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const brandingStore = useBrandingStore();
|
||||
|
||||
// State
|
||||
const showTint = ref(false);
|
||||
const isDialogOpen = ref(false);
|
||||
|
||||
// Methods
|
||||
const openBannerEditor = () => {
|
||||
isDialogOpen.value = true;
|
||||
};
|
||||
|
||||
const isCurrentCreator = computed(() => {
|
||||
return authStore.userId === brandingStore.value.id;
|
||||
});
|
||||
|
||||
</script>
|
||||
76
frontend/src/views/creators/NameTitleEditor.vue
Normal file
76
frontend/src/views/creators/NameTitleEditor.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import NameTitle from "@/views/creators/NameTitle.vue";
|
||||
import NameEditor from "@/views/creators/NameEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['closeRequested'])
|
||||
|
||||
const name = ref(props.creator.name);
|
||||
const title = ref(props.creator.title);
|
||||
|
||||
const canSave = computed(() => name != props.creator.name);
|
||||
|
||||
const client = useClient()
|
||||
const save = async () => {
|
||||
try {
|
||||
await client.post(`/api/creators/${props.creator.id}/name`);
|
||||
await client.post(`/api/creators/${props.creator.id}/title`);
|
||||
props.creator.creator.name = name;
|
||||
props.creator.title.name = title;
|
||||
emits('closeRequested')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emits('closeRequested');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">
|
||||
Modifier le Titre
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<name-editor
|
||||
:name="name"
|
||||
></name-editor>
|
||||
|
||||
<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"
|
||||
:disabled="!canSave"
|
||||
@click="save">
|
||||
Enregistrer
|
||||
</v-btn>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user