Merged PR 102: Main-Future into main
4
.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
VITE_API_URL=https://localhost:5001/
|
||||
VITE_STRIPE_API_KEY=pk_test_51OoveVDrRyqXtNdB2st1NgA8WQA9rhgGaf3q7bCpAOoQyyRS30HMCzGeHba7meVGCSPfb1BVWmOTmFOcr9MkKf5H00bLu5MqsS
|
||||
VITE_GOOGLE_CLIENT_ID=468391910875-78sfopq1t12ulrv4f5vj227j45guuj66.apps.googleusercontent.com
|
||||
VITE_FACEBOOK_APP_ID
|
||||
1474
package-lock.json
generated
@@ -11,9 +11,12 @@
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@stripe/stripe-js": "^3.0.10",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@xtiannyeto/vue-auth-social": "^0.1.9",
|
||||
"axios": "^1.6.7",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-google-login": "^2.0.26",
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 44 KiB |
13
public/images/hutopymedia/icons/tiktok.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
stroke: #000;
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: 4 2;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path id="Tiktok_1" data-name="Tiktok 1" class="cls-1" d="M820.292,990H171.646C14.581,990,11,989.648,11,821.766V174.626c0-159.964-1.459-161.5,154.584-161.5C198.026,13.122,258.91,12,291.878,12h458.7c43.88,0,15.011.37,47.358,0.37C986.933,12.37,987,17.717,987,199.3V793.727C987,989.389,946.485,990,820.292,990ZM532.342,195.936q-3.031,3.924-6.062,7.851c7.912,32.725,2.02,81.845,2.02,118.885V621.007c-5.878,25.116-21.509,52.119-39.4,63.929-65.7,43.363-157.373-18.919-133.366-104.305,7.6-27.019,36.416-53.485,62.642-59.443,14.361-3.262,33.807,2.726,44.455-2.243,5.2-22.038,1.692-65.469,2.021-94.211-96.37-32.3-186.121,59-204.091,132.344-5.533,22.583-15.541,61.3-6.062,91.968,25.379,82.1,60.537,106.81,129.325,143.56,19.407,10.368,70.144,7.051,90.932,2.243,90.864-21.02,136.493-72.949,153.573-176.085q-0.505-106.538-1.01-213.1l5.051-6.729c11.848,0.933,19.225,11.11,27.28,16.823,16.459,11.676,54.457,22.153,79.818,22.432,2.394-19.282,6.375-99.811-1.011-117.764-15.162-6.86-34.949-4.067-50.517-12.337-19.277-10.241-42.6-26.041-55.57-43.741C626.435,256.259,598.6,199.14,597,198.179,583.942,190.284,549.838,195.687,532.342,195.936Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 46 KiB |
13
public/images/hutopymedia/icons/x.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
stroke: #000;
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: 4 2;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path id="Social_x_-2_1" data-name="Social x -2 1" class="cls-1" d="M208.568,8c149.2,0.331,300.422,2.648,449.617,2.979,86.663,0,162.7-4.391,218.357,25.818,41.312,22.424,77.08,62.95,94.291,109.23,20.963,56.368,12.9,149.956,12.9,225.411V674.3c0,48.187,7.128,109.475-3.97,150.936-13.034,48.693-38.509,94.2-74.44,120.153-24.954,18.023-55.528,34.237-90.321,41.706-23.029,4.944-51.493,2.979-78.41,2.979H318.739c-46.8,0-103.154,5.907-144.909-2.979-60.19-12.809-108.811-49.038-136.97-94.335C2.042,836.752,11.055,767.026,11.055,675.3c0-117.493.992-362.125,0.992-479.618C12.047,63.677,151.59,8,208.568,8Zm10.918,201.579-2.977,1.986L436.851,500.528c-4.6,17.548-37.928,43.94-49.627,58.586L271.1,694.162c-11.979,15.019-36.369,30.637-42.679,49.65l89.328-.993,158.805-183.7c5.547,0.3,5.794,1.012,8.933,2.979,9.391,25.049,36.577,46.289,51.611,67.524,19.634,27.733,42.32,53.1,61.537,80.433,7.99,11.363,14.845,25.453,25.806,33.762H754.461c12.721,0,40.162,4,48.634-1.986L633.372,516.416c-12.208-17.35-56.689-63.437-60.544-81.426L767.364,211.565v-0.993l-3.97-.993-84.365.993L576.8,326.753c-13.733,17.111-28.64,40.051-46.649,52.629h-1.985L400.127,210.572Zm126.052,49.65,29.776,1.986L695.9,685.225l-0.993,5.958-46.649-.993q-30.765-40.212-61.537-80.433l-184.611-241.3L343.553,292c-5.976-8.018-22.454-20.757-20.843-31.776Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/hutopymedia/thumbnail/ThumbnailImage.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
38
src/App.vue
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-app v-if="isUserLoaded">
|
||||
<v-app>
|
||||
<div class="m-0 flex flex-column h-screen">
|
||||
<Header @toggle-sidebar="toggleSidebar" class="fixed w-full z-50 top-0 p-2"></Header>
|
||||
<Header class="fixed w-full z-50 top-0 p-2"></Header>
|
||||
<div class="flex flex-row relative">
|
||||
<div
|
||||
@mouseenter="openSidebar"
|
||||
@@ -10,7 +10,7 @@
|
||||
></div>
|
||||
|
||||
<transition name="slide-fade">
|
||||
<div v-show="!hideSideBar"
|
||||
<div v-show="sideBarStore.visible"
|
||||
@mouseleave="startCloseSidebarTimer"
|
||||
@mouseenter="clearCloseSidebarTimer"
|
||||
class=" fixed h-full min-w-60 border-r-2 bg-white z-30 transition-transform duration-700">
|
||||
@@ -25,44 +25,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
|
||||
<size-indicator></size-indicator>
|
||||
|
||||
</template>
|
||||
|
||||
<script async setup>
|
||||
import Header from "@/views/main/Header.vue";
|
||||
import Footer from "@/views/main/Footer.vue";
|
||||
import SideBar from "@/views/main/SideBar.vue";
|
||||
import {ref, onMounted, onUnmounted, onBeforeMount} from 'vue';
|
||||
import {eventBus} from '@/eventBus.js';
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {ref, onMounted, onUnmounted} from 'vue';
|
||||
import {useSideBarStore} from "@/stores/sideBarStore.js";
|
||||
import SizeIndicator from "@/views/tools/SizeIndicator.vue";
|
||||
|
||||
const hideSideBar = ref(true);
|
||||
const isUserLoaded = ref(false);
|
||||
const showPopup = ref(false);
|
||||
const popup = ref(null);
|
||||
const popupButton = ref(null);
|
||||
let closeSidebarTimer = null;
|
||||
|
||||
let client = useClient();
|
||||
let userStore = useUserStore();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await userStore.setCurrentUser(client);
|
||||
isUserLoaded.value = true;
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
hideSideBar.value = !hideSideBar.value;
|
||||
};
|
||||
const sideBarStore = useSideBarStore()
|
||||
|
||||
const openSidebar = () => {
|
||||
clearCloseSidebarTimer();
|
||||
hideSideBar.value = false;
|
||||
sideBarStore.show()
|
||||
};
|
||||
|
||||
const startCloseSidebarTimer = () => {
|
||||
closeSidebarTimer = setTimeout(() => {
|
||||
hideSideBar.value = true;
|
||||
sideBarStore.hide()
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@@ -83,18 +73,16 @@ const handleClickOutside = (event) => {
|
||||
!event.target.closest('.w-48') &&
|
||||
!event.target.closest('.v-app-bar-nav-icon')
|
||||
) {
|
||||
hideSideBar.value = true;
|
||||
sideBarStore.hide()
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
eventBus.value.toggleSidebar = toggleSidebar;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
eventBus.value.toggleSidebar = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const eventBus = ref({});
|
||||
64
src/internal_time_ago.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export function time_ago(time) {
|
||||
const time_date = new Date(time)
|
||||
const now_date = new Date(Date.now())
|
||||
const ago = now_date - time_date
|
||||
return internal_time_ago(ago)
|
||||
}
|
||||
|
||||
function internal_time_ago(time) {
|
||||
|
||||
switch (typeof time) {
|
||||
case 'number':
|
||||
break;
|
||||
case 'string':
|
||||
time = +new Date(time);
|
||||
break;
|
||||
case 'object':
|
||||
if (time.constructor === Date) time = time.getTime();
|
||||
break;
|
||||
default:
|
||||
time = +new Date();
|
||||
}
|
||||
|
||||
const time_formats = [
|
||||
[60, 'seconds', 1], // 60
|
||||
[120, '1 minute ago', '1 minute from now'], // 60*2
|
||||
[3600, 'minutes', 60], // 60*60, 60
|
||||
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
|
||||
[86400, 'hours', 3600], // 60*60*24, 60*60
|
||||
[172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2
|
||||
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
|
||||
[1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2
|
||||
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
|
||||
[4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2
|
||||
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
|
||||
[58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2
|
||||
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
|
||||
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
|
||||
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
|
||||
];
|
||||
|
||||
let seconds = time / 1000
|
||||
|
||||
let token = 'ago'
|
||||
let list_choice = 1
|
||||
|
||||
if (seconds === 0) {
|
||||
return 'Just now'
|
||||
}
|
||||
|
||||
if (seconds < 0) {
|
||||
seconds = Math.abs(seconds);
|
||||
token = 'from now';
|
||||
list_choice = 2;
|
||||
}
|
||||
let i = 0, format;
|
||||
while (format = time_formats[i++])
|
||||
if (seconds < format[0]) {
|
||||
if (typeof format[2] == 'string')
|
||||
return format[list_choice];
|
||||
else
|
||||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
|
||||
}
|
||||
return time;
|
||||
}
|
||||
17
src/main.js
@@ -1,6 +1,6 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import router from './router/router.js'
|
||||
import './assets/main.css'
|
||||
import {createPinia} from 'pinia'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
@@ -8,20 +8,27 @@ import 'vuetify/styles'
|
||||
import {createVuetify} from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import clientPlugin from './plugins/api.js'
|
||||
import vueGoogleOauth from 'vue3-google-login'
|
||||
import {useSubscriptionStore} from "@/stores/subscriptionStore.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {useUserStore} from "@/stores/userStore.js";
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives
|
||||
});
|
||||
|
||||
createApp(App)
|
||||
.use(clientPlugin)
|
||||
const app = createApp(App)
|
||||
.use(createPinia())
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
.use(vueGoogleOauth, {
|
||||
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
|
||||
})
|
||||
.mount('#app');
|
||||
|
||||
// this force the creation of the stores
|
||||
const subscriptionStore = useSubscriptionStore()
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import UserTransactionsModel from "@/models/userTransactionsModel.js";
|
||||
import SocialNetworksModel from "@/models/socialNetworksModel.js";
|
||||
import ProfileColorsModel from "@/models/profileColorsModel.js";
|
||||
import StoredDataUrlsModel from "@/models/storedDataUrlsModel.js";
|
||||
|
||||
export default class MyUserModel
|
||||
{
|
||||
id = "";
|
||||
firstName = "";
|
||||
lastName = "";
|
||||
userName = "";
|
||||
occupation = "";
|
||||
email = "";
|
||||
phoneNumber = "";
|
||||
birthDate = "";
|
||||
country = "";
|
||||
city = "";
|
||||
address = "";
|
||||
about = "";
|
||||
description = "";
|
||||
socialNetworks = new SocialNetworksModel();
|
||||
profileColors = new ProfileColorsModel();
|
||||
storedDataUrls = new StoredDataUrlsModel();
|
||||
totalBalance = "";
|
||||
userTransactions = [];
|
||||
|
||||
static createFromApiResult(apiResult){
|
||||
const userModel = Object.assign(new MyUserModel(), apiResult);
|
||||
|
||||
const notMapperTransaction = Object.freeze(userModel.userTransactions);
|
||||
userModel.userTransactions = [];
|
||||
|
||||
for (const transaction of notMapperTransaction) {
|
||||
userModel.userTransactions.push(UserTransactionsModel.createFromApiResult(transaction))
|
||||
}
|
||||
|
||||
return userModel;
|
||||
}
|
||||
|
||||
static getDefaultUser(){
|
||||
const defaultUser = new MyUserModel();
|
||||
defaultUser.userName = "Anonyme"
|
||||
|
||||
return defaultUser;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export default class ProfileColorsModel
|
||||
{
|
||||
bannerTop = "";
|
||||
bannerBottom = "";
|
||||
accent = "";
|
||||
menu = "";
|
||||
|
||||
static createFromApiResult(apiResult){
|
||||
return Object.assign(new ProfileColorsModel(), apiResult)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export default class SocialNetworksModel
|
||||
{
|
||||
facebookUrl = "";
|
||||
instagramUrl = "";
|
||||
xUrl = "";
|
||||
linkedInUrl = "";
|
||||
tikTokUrl = "";
|
||||
youtubeUrl = "";
|
||||
redditUrl = "";
|
||||
yourWebsiteUrl = "";
|
||||
|
||||
static createFromApiResult(apiResult){
|
||||
return Object.assign(new SocialNetworksModel(), apiResult)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default class StoredDataUrlsModel
|
||||
{
|
||||
bannerPictureUrl = "";
|
||||
profilePictureUrl = "";
|
||||
websiteIconUrl = "";
|
||||
|
||||
static createFromApiResult(apiResult){
|
||||
return Object.assign(new StoredDataUrlsModel(), apiResult)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import axios from "axios";
|
||||
import {inject} from "vue";
|
||||
import axios from "axios"
|
||||
import {useAuthStore} from "@/stores/authStore.js"
|
||||
|
||||
const key = Symbol("api");
|
||||
|
||||
export default function(app) {
|
||||
export function useClient() {
|
||||
if (!import.meta.env.VITE_API_URL) throw new Error("VITE_API_URL is not provided")
|
||||
|
||||
// You create a .env.development file and a .env file
|
||||
@@ -13,23 +11,16 @@ export default function(app) {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const requestInterceptor = (config) => {
|
||||
const token = localStorage.getItem("jwt");
|
||||
if (token) config.headers["Authorization"] = `Bearer ${token}`;
|
||||
return config;
|
||||
if (authStore.isAuthenticated) {
|
||||
config.headers["Authorization"] = `Bearer ${authStore.accessToken}`
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
api.interceptors.request.use(requestInterceptor);
|
||||
|
||||
// This is a local injection, to use it in your components you can do this:
|
||||
// const api = inject("api")
|
||||
// api.get("/some-endpoint")
|
||||
app.provide(key, api)
|
||||
}
|
||||
|
||||
export function useClient() {
|
||||
const api = inject(key)
|
||||
if (!api) throw new Error("api is not provided")
|
||||
return api;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from "@/stores/user.js";
|
||||
import GuillaumeAime from '@/views/manualusers/GuillaumeAime.vue'
|
||||
import About from '@/views/documentation/About.vue'
|
||||
import ContentPolicy from '@/views/documentation/ContentPolicy.vue'
|
||||
import FAQ from '@/views/documentation/FAQ.vue'
|
||||
@@ -12,16 +10,16 @@ import LoginView from '../views/LoginView.vue'
|
||||
import PaymentCompleted from '../views/PayementCompleted.vue'
|
||||
import SignupView from '../views/SignupView.vue'
|
||||
import Join from '../views/main/Join.vue'
|
||||
import Register from '../views/main/Register.vue'
|
||||
import Home from '../views/main/Home.vue'
|
||||
import Wallet from '../views/main/Wallet.vue'
|
||||
import Profile from '../views/main/Profile.vue'
|
||||
import ChloeBeaugrand from '../views/manualusers/ChloeProfile.vue'
|
||||
import Leffet from '../views/manualusers/LeffetProfile.vue'
|
||||
import MathieuCaron from '../views/manualusers/MathieuCaron.vue'
|
||||
import CreatorList from '../views/creators/CreatorList.vue'
|
||||
import ProfilePage from '@/views/profile/ProfilePage.vue'
|
||||
import CreatorList from '@/views/creators/CreatorList.vue'
|
||||
import CreatorPage from "@/views/creators/CreatorPage.vue";
|
||||
import ContentPage from "@/views/contents/ContentPage.vue";
|
||||
import PostContent from "@/views/contents/PostContent.vue";
|
||||
import Explorer from "@/views/explorer/explorer.vue";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -41,32 +39,11 @@ const routes = [
|
||||
path: '/@:creator',
|
||||
component: CreatorPage
|
||||
},
|
||||
{
|
||||
path: '/creators/@:creator',
|
||||
component: Profile
|
||||
},
|
||||
{
|
||||
path: '/content/post',
|
||||
component: PostContent,
|
||||
},
|
||||
|
||||
{
|
||||
path: '/@leffet',
|
||||
component: Leffet
|
||||
},
|
||||
{
|
||||
path: '/@chloebeaugrand',
|
||||
component: ChloeBeaugrand
|
||||
},
|
||||
{
|
||||
path: '/@guillaumeaime',
|
||||
component: GuillaumeAime
|
||||
},
|
||||
{
|
||||
path: '/@mathieucaron',
|
||||
component: MathieuCaron
|
||||
},
|
||||
|
||||
{
|
||||
path: '/helpandcontact',
|
||||
component: HelpAndContact,
|
||||
@@ -114,17 +91,11 @@ const routes = [
|
||||
component: Join,
|
||||
meta: { hideSideBar: true }
|
||||
},
|
||||
|
||||
{
|
||||
path: '/paymentcompleted',
|
||||
name: 'PayementCompleted',
|
||||
component: PaymentCompleted
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: Profile,
|
||||
meta: { requiresAuth: true }
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: Register,
|
||||
meta: { hideSideBar: true }
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
@@ -136,12 +107,29 @@ const routes = [
|
||||
name: 'login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/paymentcompleted',
|
||||
name: 'PayementCompleted',
|
||||
component: PaymentCompleted,
|
||||
},
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'wallet',
|
||||
component: Wallet,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: ProfilePage,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/explorer',
|
||||
name: 'explorer',
|
||||
component: Explorer,
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
@@ -151,11 +139,10 @@ const router = createRouter({
|
||||
|
||||
// Navigation gards
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useUserStore();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (!authStore.user.value || !Object.keys(authStore.user.value).length) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
next('/');
|
||||
} else {
|
||||
next();
|
||||
@@ -1,67 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
const baseUrl = '/api';
|
||||
|
||||
export const auth = defineStore({
|
||||
id: 'auth',
|
||||
|
||||
state: () => ({
|
||||
user: "",
|
||||
refreshTokenTimeout: 0
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// TODO: Fix login methods
|
||||
|
||||
async login(client, email, password) {
|
||||
const requestBody = {
|
||||
email: email,
|
||||
password: password
|
||||
};
|
||||
const response = await client.post(`${baseUrl}/users/login`, requestBody)
|
||||
this.user = {
|
||||
accessToken: response.data.accessToken,
|
||||
refreshToken: response.data.refreshToken,
|
||||
}
|
||||
localStorage.setItem('jwt', this.user.accessToken);
|
||||
|
||||
this.startRefreshTokenTimer();
|
||||
},
|
||||
|
||||
async loginGoogle(client, accessToken) {
|
||||
const response = await client.post(`${baseUrl}/google/sign-in`, {accessToken: accessToken})
|
||||
this.user = {
|
||||
accessToken: response.data.accessToken,
|
||||
refreshToken: response.data.refreshToken,
|
||||
email: response.data.email
|
||||
}
|
||||
localStorage.setItem('jwt', this.user.accessToken);
|
||||
|
||||
this.startRefreshTokenTimer();
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.setItem('jwt', '');
|
||||
this.user = null;
|
||||
|
||||
this.stopRefreshTokenTimer();
|
||||
},
|
||||
|
||||
async refreshToken(client) {
|
||||
const response = await client.post(`${baseUrl}/users/refresh`);
|
||||
this.user.accessToken = response.accessToken;
|
||||
localStorage.setItem('jwt', this.user.accessToken);
|
||||
|
||||
this.startRefreshTokenTimer();
|
||||
},
|
||||
|
||||
startRefreshTokenTimer() {
|
||||
const timeout = 50 * 1000;
|
||||
this.refreshTokenTimeout = setTimeout(this.refreshToken, timeout);
|
||||
},
|
||||
|
||||
stopRefreshTokenTimer() {
|
||||
clearTimeout(this.refreshTokenTimeout);
|
||||
}
|
||||
}
|
||||
});
|
||||
100
src/stores/authStore.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useSessionStorage} from "@vueuse/core";
|
||||
import {jwtDecode} from "jwt-decode";
|
||||
|
||||
function getClaimsFromToken(token) {
|
||||
try {
|
||||
return jwtDecode(token);
|
||||
} catch (error) {
|
||||
console.error('Invalid token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore(
|
||||
'auth',
|
||||
() => {
|
||||
const clientApi = useClient()
|
||||
const router = useRouter()
|
||||
|
||||
const accessToken = useSessionStorage('auth-accessToken', undefined)
|
||||
const refreshToken = useSessionStorage('auth-refreshToken', undefined)
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value)
|
||||
const userId = computed(() => {
|
||||
const claims = getClaimsFromToken(accessToken.value)
|
||||
return claims.sub;
|
||||
})
|
||||
|
||||
function updateTokens(data) {
|
||||
accessToken.value = data.accessToken
|
||||
refreshToken.value = data.refreshToken
|
||||
}
|
||||
|
||||
function cleanTokens() {
|
||||
updateTokens({
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
cleanTokens()
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
async function login(email, password) {
|
||||
try {
|
||||
const response = await clientApi.post(
|
||||
'api/users/login',
|
||||
{
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
updateTokens(response.data)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
cleanTokens()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function loginGoogle(accessToken) {
|
||||
try {
|
||||
const response = await clientApi.post(
|
||||
'api/google/sign-in',
|
||||
{
|
||||
accessToken: accessToken
|
||||
})
|
||||
updateTokens(response.data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
cleanTokens()
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const response = await clientApi.post(
|
||||
'api/users/refresh',
|
||||
{
|
||||
refreshToken: refreshToken
|
||||
});
|
||||
|
||||
updateTokens({
|
||||
accessToken: response.accessToken,
|
||||
refreshToken: refreshToken
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
cleanTokens()
|
||||
}
|
||||
}
|
||||
|
||||
return {accessToken, refreshToken, isAuthenticated, userId, login, loginGoogle, logout}
|
||||
})
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
23
src/stores/sideBarStore.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import {computed, ref} from 'vue';
|
||||
import {defineStore} from "pinia";
|
||||
|
||||
export const useSideBarStore = defineStore(
|
||||
'sideBar',
|
||||
() => {
|
||||
const state = ref(false)
|
||||
const visible = computed(() => state.value)
|
||||
|
||||
function toggle() {
|
||||
state.value = !state.value
|
||||
}
|
||||
|
||||
function show() {
|
||||
state.value = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
state.value = false
|
||||
}
|
||||
|
||||
return {visible, toggle, show, hide}
|
||||
})
|
||||
71
src/stores/subscriptionStore.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {useSessionStorage} from "@vueuse/core";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {watch} from "vue";
|
||||
|
||||
export const useSubscriptionStore = defineStore(
|
||||
'subscription',
|
||||
() => {
|
||||
|
||||
const subscriptions = useSessionStorage(
|
||||
'subscription-subscriptions',
|
||||
{})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const authWatcher = watch(
|
||||
() => authStore.isAuthenticated,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
await loadSubscriptions()
|
||||
} else {
|
||||
subscriptions.value = {}
|
||||
}
|
||||
})
|
||||
|
||||
function isSubscribeTo(creatorId) {
|
||||
return !!subscriptions.value[creatorId];
|
||||
}
|
||||
|
||||
async function subscribeTo(creatorId) {
|
||||
try {
|
||||
const client = useClient()
|
||||
const response = await client.post(
|
||||
`/api/creators/${creatorId}/subscribe`
|
||||
);
|
||||
subscriptions.value[creatorId] = response.data;
|
||||
} catch (error) {
|
||||
console.error("Error subscribing to creator", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribeFrom(creatorId) {
|
||||
try {
|
||||
const client = useClient()
|
||||
const response = await client.post(
|
||||
`/api/creators/${creatorId}/unsubscribe`
|
||||
);
|
||||
delete subscriptions.value[creatorId];
|
||||
} catch (error) {
|
||||
console.error("Error unsubscribing from creator", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubscriptions() {
|
||||
try {
|
||||
const client = useClient()
|
||||
const response = await client.get(`/api/subscriptions`);
|
||||
|
||||
subscriptions.value = response.data.reduce(
|
||||
(acc, sub) => {
|
||||
acc[sub.creatorId] = sub;
|
||||
return acc;
|
||||
},
|
||||
{});
|
||||
} catch (error) {
|
||||
console.error("Error loading subscriptions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {subscriptions, isSubscribeTo, subscribeTo, unsubscribeFrom}
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import MyUserModel from "@/models/myUserModel.js";
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const user = ref({});
|
||||
const hasChanged = ref(false);
|
||||
|
||||
function getCurrentUser() {
|
||||
return this.user.value;
|
||||
}
|
||||
async function setCurrentUser(client) {
|
||||
try {
|
||||
const myUser = await client.get("/api/GetMyUser");
|
||||
this.user.value = MyUserModel.createFromApiResult(myUser.data);
|
||||
this.hasChanged = false;
|
||||
} catch (e){
|
||||
this.user.value = MyUserModel.getDefaultUser();
|
||||
console.log("User not logged.")
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCurrentUser(client, myUserModel, profilePicture, bannerPicture, websiteIcon) {
|
||||
await client.patch("/api/UpdateMyUser/profile", myUserModel)
|
||||
|
||||
if (typeof myUserModel.storedDataUrls.profilePictureUrl !== "object") {
|
||||
const haveNewProfilePicture = profilePicture !== null && profilePicture.size !== 0;
|
||||
const updateProfilePictureEndpoint = haveNewProfilePicture ? `/api/UpdateMyUser/profile-picture` : `/api/UpdateMyUser/profile-picture?url=${myUserModel.storedDataUrls.profilePictureUrl}`;
|
||||
const response = await client.post(updateProfilePictureEndpoint, profilePicture, {
|
||||
headers: {
|
||||
'Content-Type': profilePicture?.type ?? "application/octet-stream",
|
||||
}
|
||||
});
|
||||
|
||||
if (haveNewProfilePicture) {
|
||||
this.user.value.storedDataUrls.profilePictureUrl = response.data;
|
||||
}
|
||||
}
|
||||
if (typeof myUserModel.storedDataUrls.bannerPictureUrl !== "object") {
|
||||
const haveNewBannerPicture = bannerPicture !== null && bannerPicture.size !== 0;
|
||||
|
||||
const updateBannerPictureEndpoint = haveNewBannerPicture ? `/api/UpdateMyUser/banner-picture` : `/api/UpdateMyUser/banner-picture?url=${myUserModel.storedDataUrls.bannerPictureUrl}`;
|
||||
const response = await client.post(updateBannerPictureEndpoint, bannerPicture, {
|
||||
headers: {
|
||||
'Content-Type': bannerPicture?.type ?? "octet-stream",
|
||||
}
|
||||
});
|
||||
|
||||
if (haveNewBannerPicture) {
|
||||
this.user.value.storedDataUrls.bannerPictureUrl = response.data;
|
||||
}
|
||||
}
|
||||
if (typeof myUserModel.storedDataUrls.websiteIconUrl !== "object") {
|
||||
const haveNewWebsiteIcon = websiteIcon !== null && websiteIcon.size !== 0;
|
||||
|
||||
const updateWebsiteIconEndpoint = haveNewWebsiteIcon ? `/api/UpdateMyUser/website-icon` : `/api/UpdateMyUser/website-icon?url=${myUserModel.storedDataUrls.websiteIconUrl}`;
|
||||
const response = await client.post(updateWebsiteIconEndpoint, websiteIcon, {
|
||||
headers: {
|
||||
'Content-Type': websiteIcon?.type ?? "application/octet-stream",
|
||||
}
|
||||
});
|
||||
|
||||
if (haveNewWebsiteIcon) {
|
||||
this.user.value.storedDataUrls.websiteIconUrl = response.data;
|
||||
}
|
||||
}
|
||||
|
||||
this.user.value = myUserModel;
|
||||
this.hasChanged = true;
|
||||
}
|
||||
|
||||
return { user, getCurrentUser, setCurrentUser, updateCurrentUser }
|
||||
})
|
||||
67
src/stores/userStore.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import {computed, watch} from 'vue'
|
||||
import {defineStore} from 'pinia'
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useSessionStorage} from "@vueuse/core";
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'user',
|
||||
() => {
|
||||
const authStore = useAuthStore()
|
||||
const authWatcher = watch(
|
||||
() => authStore.isAuthenticated,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
await fetchCurrentUserProfile()
|
||||
} else {
|
||||
user.value = undefined
|
||||
creator.value = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const user = useSessionStorage(
|
||||
'user-user',
|
||||
{},
|
||||
{writeDefaults: false})
|
||||
const creator = useSessionStorage(
|
||||
'user-creator',
|
||||
{},
|
||||
{writeDefaults: false})
|
||||
|
||||
const hasCreator = computed(() =>
|
||||
creator.value
|
||||
&& Object.getOwnPropertyNames(creator.value).length >= 1)
|
||||
|
||||
const alias = computed(() => {
|
||||
if (user.value) {
|
||||
return user.value.alias || `${user.value.firstName || ''} ${user.value.lastName || ''}`.trim() || 'Anonyme'
|
||||
}
|
||||
return 'Anonyme';
|
||||
})
|
||||
const portraitUrl = computed(() => {
|
||||
return user.value && user.value.portraitUrl
|
||||
? user.value.portraitUrl
|
||||
: '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'
|
||||
})
|
||||
|
||||
async function fetchCurrentUserProfile() {
|
||||
try {
|
||||
const client = useClient()
|
||||
const userResponse = await client.get("/api/GetMyUser");
|
||||
user.value = userResponse.data
|
||||
|
||||
try {
|
||||
const creatorId = userResponse.data.id
|
||||
const creatorResponse = await client.get(`/api/creators/${creatorId}`)
|
||||
creator.value = creatorResponse.data
|
||||
} catch (error) {
|
||||
creator.value = undefined
|
||||
}
|
||||
} catch (error) {
|
||||
user.value = undefined;
|
||||
creator.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {user, creator, alias, hasCreator, portraitUrl, fetchCurrentUserProfile}
|
||||
})
|
||||
@@ -1,113 +1,26 @@
|
||||
<template>
|
||||
<div class="hidden sm:block" style="height: 40px"></div>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col lg:flex-row items-center justify-center">
|
||||
<div class="max-w-[700px] min-w-[300px]">
|
||||
<img class="rounded-none sm:rounded-2xl sm:w-full mr-8" src="/images/hutopymedia/loginpage/loginhutopy.png" alt="hutopy login">
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center min-w-[300px] m-12">
|
||||
<h1 class="text-center text-2xl font-bold mb-5">Connexion</h1>
|
||||
|
||||
<google-login class="w-full" :callback="googleCallback" popup-type="TOKEN">
|
||||
<template #default>
|
||||
<v-btn density="comfortable" class="mb-2 w-full">
|
||||
<v-icon left>mdi-google</v-icon>
|
||||
Google
|
||||
</v-btn>
|
||||
</template>
|
||||
</google-login>
|
||||
|
||||
<!-- <v-btn density="comfortable" class="mb-2 w-full">-->
|
||||
<!-- <v-icon left>mdi-facebook</v-icon>-->
|
||||
<!-- Facebook-->
|
||||
<!-- </v-btn>-->
|
||||
|
||||
<div class="w-full h-0.5 mt-4 mb-4" :style="{ backgroundColor: '#A30E79' }"></div>
|
||||
|
||||
<v-btn density="comfortable" class="mb-2 w-full" @click="showEmailForm = !showEmailForm">
|
||||
<v-icon left>mdi-account</v-icon>
|
||||
Utilisateur
|
||||
</v-btn>
|
||||
|
||||
<div v-if="showEmailForm" class="w-full mt-2">
|
||||
<v-text-field v-model="user.email"
|
||||
label="Courriel"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-email"
|
||||
color="transparent"
|
||||
class="text-black"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field v-model="user.password"
|
||||
label="Mot de passe"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-lock"
|
||||
append-inner-icon="mdi-eye"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
color="transparent"
|
||||
class="text-black"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn class="w-full text-center text-white" :style="{ backgroundColor: '#A30E79' }" @click="login">Connecter</v-btn>
|
||||
|
||||
<p class="mt-4 text-sm text-center">
|
||||
Si vous n'avez pas de compte, <a href="/register" class="text-blue-500">cliquez ici</a> pour en créer un.
|
||||
</p>
|
||||
|
||||
<div v-if="errorSnackBar" class="mb-4 text-red-600">
|
||||
Nom d'utilisateur ou mot de passe invalide.
|
||||
<button class="text-red-600 ml-4" @click="errorSnackBar = false">Fermer</button>
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row items-center justify-center">
|
||||
<div class="max-w-[700px] min-w-[300px]">
|
||||
<img class="rounded-none sm:rounded-2xl sm:w-full mr-8" src="/images/hutopymedia/loginpage/loginhutopy.png" alt="hutopy login">
|
||||
</div>
|
||||
<div class="flex flex-col items-center min-w-[300px] m-12">
|
||||
<login-form :onSuccess="handleSuccess"></login-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<selected-footer></selected-footer>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import { auth } from '@/stores/auth.js';
|
||||
import { GoogleLogin } from "vue3-google-login";
|
||||
// import { FacebookAuth } from '@xtiannyeto/vue-auth-social';
|
||||
import SelectedFooter from "@/views/main/SelectedFooter.vue";
|
||||
import LoginForm from "@/views/main/LoginForm.vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const api = useClient();
|
||||
const store = auth();
|
||||
const router = useRouter();
|
||||
|
||||
const user = ref({});
|
||||
const errorSnackBar = ref(false);
|
||||
const showEmailForm = ref(false);
|
||||
const showPassword = ref(false);
|
||||
|
||||
async function login() {
|
||||
try {
|
||||
await store.login(api, user.value.email, user.value.password);
|
||||
await router.push('/');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
errorSnackBar.value = true;
|
||||
}
|
||||
const handleSuccess = async () => {
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
const googleCallback = async (response) => {
|
||||
await store.loginGoogle(api, response["access_token"]);
|
||||
await router.push("/");
|
||||
};
|
||||
|
||||
// const facebookAppId = import.meta.env.VITE_FACEBOOK_APP_ID;
|
||||
// const facebookCallback = (response) => {
|
||||
// console.log("User Successfully Logged In", response);
|
||||
// };
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,157 @@
|
||||
<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-item @click="reportContent">
|
||||
<v-list-item-title>Reporter 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>
|
||||
|
||||
<div class=" shadow-lg rounded-lg max-w-sm">
|
||||
|
||||
<div class="h-48 object-cover bg-purple">
|
||||
|
||||
<v-img :src="props.content.url"
|
||||
v-if="!isHttpUrl">
|
||||
</v-img>
|
||||
|
||||
<iframe v-if="isHttpUrl"
|
||||
:src="props?.content?.uri"
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<router-link :to="'content/' + props?.content?.id">
|
||||
<div class="px-4">
|
||||
<div class="flex justify-around py-2">
|
||||
<v-btn variant="plain" icon @click="likeContent">
|
||||
<v-icon>mdi-thumb-up-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn variant="plain" icon @click="dislikeContent">
|
||||
<v-icon>mdi-thumb-down-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:class="{'comment-active': hasMessages}"
|
||||
variant="plain"
|
||||
icon
|
||||
@click="toggleComments">
|
||||
<v-icon>mdi-comment-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<div class="text-lg font-bold">{{ props.content.title }}</div>
|
||||
<div class="text-sm text-gray-500">{{ props.content.description }}</div>
|
||||
</router-link>
|
||||
<donation-button :color-border="colorMenu"
|
||||
:color-accent="colorAccent"
|
||||
:creator-id="creatorId"
|
||||
:creator-name="creatorName"
|
||||
:creator-logo="creatorLogo"
|
||||
iconColorClass="text-black"
|
||||
></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">
|
||||
Supprimer ce contenu ?
|
||||
</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 {defineProps, computed} from 'vue';
|
||||
|
||||
const isHttpUrl = computed(() => props.content?.uri?.startsWith('http'))
|
||||
import {computed, 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";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
@@ -42,4 +160,98 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
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 hasUrls = computed(() => !!props.content.urls && props.content.urls.length > 0);
|
||||
const messagesVisible = ref(false);
|
||||
const messages = ref([]);
|
||||
const hasMessages = computed(() => messages.value.length > 0)
|
||||
|
||||
function openDeleteConfirmationDialog() {
|
||||
openDeleteConfirmationModal.value = true;
|
||||
}
|
||||
|
||||
function addMessage(newMessage) {
|
||||
messages.value.unshift(newMessage);
|
||||
messagesVisible.value = true;
|
||||
}
|
||||
|
||||
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 reportContent() {
|
||||
console.log('Reporter le contenu');
|
||||
}
|
||||
|
||||
function redirectToContent() {
|
||||
window.location.href = `/content/${props.content.id}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border-color: #EAEBEC;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-active .v-icon {
|
||||
color: #D63DAB;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,97 @@
|
||||
<template>
|
||||
|
||||
<div class="flex">
|
||||
<ContentCard v-for="content in contents"
|
||||
:content="content"
|
||||
class="m-2 bg-red w-full">
|
||||
</ContentCard>
|
||||
</div>
|
||||
<v-infinite-scroll :items="contents"
|
||||
:onLoad="fetchContents"
|
||||
class="gap-2">
|
||||
|
||||
<template v-for="content in contents" :key="content">
|
||||
<content-card :content="content"
|
||||
@content-deleted="onContentDeleted"
|
||||
></content-card>
|
||||
</template>
|
||||
|
||||
<template v-slot:empty>
|
||||
Il n'y a pas plus de contenus
|
||||
</template>
|
||||
|
||||
<template v-slot:error>
|
||||
<v-alert type="error">{{ errorMessage }}</v-alert>
|
||||
</template>
|
||||
|
||||
</v-infinite-scroll>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {defineProps, onBeforeMount, ref} from 'vue';
|
||||
import {ref, watch} from 'vue';
|
||||
import ContentCard from "./ContentCard.vue";
|
||||
|
||||
const props = defineProps({
|
||||
creatorId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
contents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const client = useClient();
|
||||
const contents = ref();
|
||||
const client = useClient()
|
||||
const contents = ref(props.contents)
|
||||
const errorMessage = ref()
|
||||
let last_id = null
|
||||
|
||||
onBeforeMount(async () => {
|
||||
async function onContentDeleted(contentId) {
|
||||
contents.value = contents.value.filter(c => c.id !== contentId)
|
||||
}
|
||||
|
||||
const creatorIdWatcher = watch(
|
||||
() => props.creatorId,
|
||||
(newCreatorId) => {
|
||||
if (newCreatorId) {
|
||||
// Reset contents and last_id when the creatorId changes
|
||||
contents.value = []
|
||||
last_id = null
|
||||
// Fetch contents for the new creator
|
||||
fetchContents({
|
||||
done: () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchContents({done, page_size = 10}) {
|
||||
if (props.creatorId == null) return
|
||||
|
||||
try {
|
||||
const response = await client.get(`/api/contents/user/${props.creatorId}`)
|
||||
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) {
|
||||
contents.value = response.data
|
||||
console.table(contents.value)
|
||||
|
||||
const contentCount = response.data.length
|
||||
|
||||
if (contentCount > 0) {
|
||||
contents.value.push(...response.data)
|
||||
const [last_content] = response.data.slice(-1)
|
||||
last_id = last_content.id
|
||||
}
|
||||
|
||||
if (contentCount < page_size)
|
||||
done('empty')
|
||||
else
|
||||
done('ok')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch posts", error);
|
||||
errorMessage.value = error.message || "Failed to fetch contents";
|
||||
done('error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,62 +1,143 @@
|
||||
<template>
|
||||
<div class="flex h-[calc(100vh-118px)] -mt-2">
|
||||
<!-- Homemade carousel -->
|
||||
<div class="flex-1 flex items-center justify-center bg-neutral-500 max-h-screen relative">
|
||||
|
||||
<div class="flex flex-column w-full">
|
||||
<!-- 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>
|
||||
|
||||
<div class="border-b-2 p-6 font-sans space-y-2">
|
||||
<div v-if="data && data.uri">
|
||||
Uri: {{ data?.uri }}
|
||||
<!-- 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>
|
||||
<div v-if="data && data.title" class="font-semibold">
|
||||
Title: {{ data?.title }}
|
||||
|
||||
<!-- 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 v-if="data && data.description">
|
||||
Description: {{ data?.description }}
|
||||
|
||||
<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>
|
||||
<div v-if="data && data.createdAt">
|
||||
Date: {{ data?.createdAt }}
|
||||
</div>
|
||||
<div v-if="data && data.createdBy">
|
||||
Creator: {{ data?.createdBy }}
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<div class="border-b-2 p-6">
|
||||
<PostMessage :content-id="contentId">
|
||||
</PostMessage>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div class="fixed-width border-l-2 p-6 bg-white overflow-y-auto max-h-screen">
|
||||
|
||||
<div class="border-b-2 p-6">
|
||||
<h2 class="font-sans font-semibold">Commentaires</h2>
|
||||
<MessageList :content-id="contentId">
|
||||
</MessageList>
|
||||
</div>
|
||||
<div class="border-b-2 p-6 font-sans space-y-2">
|
||||
<div v-if="data && data.createdByName">
|
||||
Créé par: {{ data.createdByName }}
|
||||
</div>
|
||||
<div v-if="data && data.title" class="font-semibold">
|
||||
Titre: {{ data.title }}
|
||||
</div>
|
||||
<div v-if="data && data.description">
|
||||
Description: {{ data.description }}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-around py-2">
|
||||
<v-btn variant="plain" icon @click="likeContent">
|
||||
<v-icon :color="'#313131'">mdi-thumb-up-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn variant="plain" icon @click="dislikeContent">
|
||||
<v-icon :color="'#000000'">mdi-thumb-down-outline</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<donation-button v-if="data"
|
||||
:color-border="data.colorMenu"
|
||||
:color-accent="data.colorAccent"
|
||||
:creator-id="data.createdBy"
|
||||
:creator-name="data.createdByName"
|
||||
:creator-logo="data.createdByPortraitUrl"
|
||||
iconColorClass="text-black"
|
||||
></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 async>
|
||||
<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 {onMounted, watch, ref, computed} from 'vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
|
||||
const data = ref()
|
||||
const data = ref(null);
|
||||
const currentImageIndex = ref(0);
|
||||
|
||||
const route = useRoute()
|
||||
const client = useClient()
|
||||
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
|
||||
const response = await client.get(`/api/contents/${contentId}`);
|
||||
data.value = response.data;
|
||||
console.table(data.value)
|
||||
} catch (error) {
|
||||
console.error(`Error fetching content: ${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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,5 +151,20 @@ watch(contentId, (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>
|
||||
|
||||
31
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>
|
||||
@@ -10,11 +10,11 @@
|
||||
<v-form>
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
label="Select Image"
|
||||
label="Choisisez votre contenu"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
@change="onFileSelected">
|
||||
</v-file-input>
|
||||
@change="onFileSelected"
|
||||
></v-file-input>
|
||||
|
||||
<v-img
|
||||
v-if="url"
|
||||
@@ -58,7 +58,7 @@
|
||||
// import posts from "@/views/posts/posts.json";
|
||||
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {defineProps, ref} from 'vue';
|
||||
import {ref} from 'vue';
|
||||
import PostContentMenu from "@/views/contents/PostContentMenu.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -66,14 +66,14 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const client = useClient();
|
||||
const client = useClient()
|
||||
|
||||
const selectedFile = ref("");
|
||||
const url = ref("");
|
||||
const title = ref("");
|
||||
const description = ref("");
|
||||
const selectedFile = ref("")
|
||||
const url = ref("")
|
||||
const title = ref("")
|
||||
const description = ref("")
|
||||
|
||||
const onFileSelected = () => {
|
||||
if (selectedFile.value) {
|
||||
@@ -99,7 +99,6 @@ const publish = async () => {
|
||||
} else {
|
||||
console.error(`Failed to create content ${response.data}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
138
src/views/contents/PublishContentButton.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script setup>
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {ref} from 'vue';
|
||||
import {useUserStore} from '@/stores/userStore.js';
|
||||
import {v7} from 'uuid';
|
||||
|
||||
const props = defineProps({
|
||||
creator: {type: Object, required: true},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['content-posted'])
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isDialogActive = ref(false);
|
||||
|
||||
const client = useClient();
|
||||
const title = ref('');
|
||||
const message = ref('');
|
||||
const files = ref([]);
|
||||
|
||||
async function publishPost() {
|
||||
const formData = new FormData();
|
||||
formData.append('id', v7());
|
||||
formData.append('creatorId', userStore.user.id);
|
||||
formData.append('title', title.value);
|
||||
formData.append('description', message.value);
|
||||
files.value.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
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
|
||||
v-if="creator && userStore.user && creator.id === userStore.user.id"
|
||||
class="flex items-center text-white transform transition-transform duration-200 hover:text-gray-300 hover:scale-125 px-4"
|
||||
@click="isDialogActive = true">
|
||||
<v-icon style="font-size: 35px; height: 35px; 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 ${creator.colors.menu}`
|
||||
}">
|
||||
|
||||
<div class="py-4 text-2xl font-bold border-b mb-2">
|
||||
Créer un Contenu
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-center px-3">
|
||||
<img
|
||||
:src="userStore.portraitUrl"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="40"
|
||||
height="40"
|
||||
:style="{
|
||||
border: `2px solid ${creator.colors.accent}`
|
||||
}">
|
||||
<div class="capitalize px-2 text-2xl">{{ userStore.creator.name }}</div>
|
||||
<v-btn icon @click="cancelPost" class="ml-auto" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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-btn variant="outlined" :style="{ borderColor: creator.colors.menu, color: creator.colors.menu }"
|
||||
@click="publishPost" class="w-full">
|
||||
Publier
|
||||
</v-btn>
|
||||
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
26
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>
|
||||
@@ -1,40 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "00000002-0000-0000-0000-000000000001",
|
||||
"url": "https://www.youtube.com/embed/neKWqjE0eSs?si=Bo7xbYaw9-56w3lU",
|
||||
"description": "Bonjour, Nous sommes fiers de vous présenter la version 0.10 d'Hutopy. Nous sommes au début d'une aventure visant à transformer la sphère des médias sociaux. Notre objectif est d'apporter un souffle de fraîcheur en favorisant des interactions plus constructives entre les individus. Dans cette première version, seuls nos utilisateurs testeurs ont accès à la plateforme comme créateur. Dans un futur proche, avec le déploiement de la version 0.2, nous contacterons les personnes qui se sont inscrites via l'onglet d'inscription et ont rempli quelques questions. Si vous souhaitez soutenir le développement de la plateforme, deux options s'offrent à vous. La première consiste à nous apporter un soutien financier, tandis que la seconde est de nous contacter pour faire partie de l'équipe de développement. Nous recherchons actuellement un programmeur supplémentaire pour faire progresser certaines fonctionnalités. Merci de visiter Hutopy.",
|
||||
"created_by": {
|
||||
"id": "00000001-0000-0000-0000-000000000001",
|
||||
"alias": "@marchy"
|
||||
},
|
||||
"created_at": "",
|
||||
"thumb_up_count": 0,
|
||||
"thumb_down_count": 0
|
||||
},
|
||||
{
|
||||
"id": "00000002-0000-0000-0000-000000000002",
|
||||
"url": "/images/usersmedia/HutopyProfile/pictures/version.png",
|
||||
"title": "Déploiement de la version 0.10 d'Hutopy",
|
||||
"description": "Bonjour, Nous sommes fiers de vous présenter la version 0.10 d'Hutopy. Nous sommes au début d'une aventure visant à transformer la sphère des médias sociaux. Notre objectif est d'apporter un souffle de fraîcheur en favorisant des interactions plus constructives entre les individus. Dans cette première version, seuls nos utilisateurs testeurs ont accès à la plateforme comme créateur. Dans un futur proche, avec le déploiement de la version 0.2, nous contacterons les personnes qui se sont inscrites via l'onglet d'inscription et ont rempli quelques questions. Si vous souhaitez soutenir le développement de la plateforme, deux options s'offrent à vous. La première consiste à nous apporter un soutien financier, tandis que la seconde est de nous contacter pour faire partie de l'équipe de développement. Nous recherchons actuellement un programmeur supplémentaire pour faire progresser certaines fonctionnalités. Merci de visiter Hutopy.",
|
||||
"created_by": {
|
||||
"id": "00000001-0000-0000-0000-000000000001",
|
||||
"alias": "@marchy",
|
||||
"portrait_url": ""
|
||||
},
|
||||
"created_at": "",
|
||||
"thumb_up_count": 0,
|
||||
"thumb_down_count": 0
|
||||
},
|
||||
{
|
||||
"id": "00000002-0000-0000-0000-000000000003",
|
||||
"url": "/images/usersmedia/HutopyProfile/pictures/version.png",
|
||||
"description": "Bonjour, Nous sommes fiers de vous présenter la version 0.10 d'Hutopy. Nous sommes au début d'une aventure visant à transformer la sphère des médias sociaux. Notre objectif est d'apporter un souffle de fraîcheur en favorisant des interactions plus constructives entre les individus. Dans cette première version, seuls nos utilisateurs testeurs ont accès à la plateforme comme créateur. Dans un futur proche, avec le déploiement de la version 0.2, nous contacterons les personnes qui se sont inscrites via l'onglet d'inscription et ont rempli quelques questions. Si vous souhaitez soutenir le développement de la plateforme, deux options s'offrent à vous. La première consiste à nous apporter un soutien financier, tandis que la seconde est de nous contacter pour faire partie de l'équipe de développement. Nous recherchons actuellement un programmeur supplémentaire pour faire progresser certaines fonctionnalités. Merci de visiter Hutopy.",
|
||||
"created_by": {
|
||||
"id": "00000001-0000-0000-0000-000000000001",
|
||||
"alias": "@marchy"
|
||||
},
|
||||
"created_at": "",
|
||||
"thumb_up_count": 0,
|
||||
"thumb_down_count": 0
|
||||
}
|
||||
]
|
||||
65
src/views/creators/CreatorAbout.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
class="flex items-center text-white transform transition-transform duration-200 hover:text-gray-300 hover:scale-125 mr-2"
|
||||
@click="AboutUs = true"
|
||||
>
|
||||
<v-icon style="font-size: 35px; height: 35px; width: 55px;">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
</button>
|
||||
|
||||
<v-dialog v-model="AboutUs" max-width="500px">
|
||||
<v-form>
|
||||
<v-card class="text-center rounded-xl"
|
||||
:style="{
|
||||
border: `3px solid ${creator.colors.menu}`
|
||||
}">
|
||||
|
||||
<div class="flex items-center justify-between py-4 text-2xl font-bold border-b mb-2">
|
||||
<div class="flex-1 text-center">
|
||||
{{ creator.about.title }}
|
||||
</div>
|
||||
|
||||
<v-btn icon @click="AboutUs = false" class="ml-auto mr-2" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-card-text class="scrollable-content">
|
||||
{{ creator.about.description }}
|
||||
</v-card-text>
|
||||
|
||||
<div class="p-4">
|
||||
<v-btn variant="outlined" text class=" w-full" @click="AboutUs = false" :style="{ borderColor: creator.colors.menu, color: creator.colors.menu }">Fermer</v-btn>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
const AboutUs = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
creator: {type: Object, required: true},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scrollable-content {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scrollable-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,182 +1,173 @@
|
||||
<template>
|
||||
<!-- Bannière-->
|
||||
<div class="relative bottom-4 z-20 m">
|
||||
<div class="relative flex flex-col">
|
||||
<!-- Social Network-->
|
||||
<div class="bg-opacity-50 flex flex-col md:flex-row items-center justify-between py-2 px-4 min-h-24" :style="{ backgroundColor: creator.profileColors.bannerTop || '#6B0065' }">
|
||||
<div class="text-white mb-2 md:mb-0 w-full md:w-auto flex justify-between md:block">
|
||||
<div class="text-lg">1000+ Abonnés</div>
|
||||
<div class="text-sm">500 Contacts en commun</div>
|
||||
<div>
|
||||
<div class="relative z-20">
|
||||
<div class="relative flex flex-col">
|
||||
<!-- Social Network-->
|
||||
<div class="bg-opacity-50 flex flex-col md:flex-row items-center py-4 px-4 min-h-14"
|
||||
:style="{ backgroundColor: creator.colors.bannerTop || '#6B0065' }">
|
||||
<div class="w-full flex justify-between lg:justify-end gap-14 mt-2">
|
||||
<a
|
||||
v-for="socialNetwork in GetSocialsUrls()"
|
||||
:href="socialNetwork.url"
|
||||
target="_blank"
|
||||
class="text-white text-2xl 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-if="socialNetwork.icon.includes('tiktok')" :src="socialNetwork.icon" class="w-9 h-9"
|
||||
alt="Tiktok">
|
||||
<img v-if="socialNetwork.icon.includes('websiteIcon')" :src="socialNetwork.icon" class="w-9 h-9"
|
||||
alt="Website">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-6 md:flex flex-wrap space-x-0 md:space-x-10 gap-6 ">
|
||||
<a
|
||||
v-for="socialNetwork in GetActiveSocialNetworkUrls()"
|
||||
:href="socialNetwork.url" target="_blank"
|
||||
class="text-white text-2xl 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-if="socialNetwork.icon.includes('tiktok')" :src="socialNetwork.icon" class="w-9 h-9" alt="Tiktok">
|
||||
<img v-if="socialNetwork.icon.includes('websiteIcon')" :src="socialNetwork.icon" class="w-9 h-9" alt="Website">
|
||||
</a>
|
||||
|
||||
<!--Banner & user info-->
|
||||
<div class="relative">
|
||||
<!--Banner-->
|
||||
<div>
|
||||
<img class=" w-full drop-shadow-[0_15px_10px_rgba(0,0,0,0.7)]"
|
||||
:src="creator.images.banner"
|
||||
alt="Profile Banner" style="max-height: 425px">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Button & image profile -->
|
||||
<div class="relative bottom-4 w-full">
|
||||
<div class="bg-gray-800 py-4 relative shadow-lg min-h-24"
|
||||
:style="{ backgroundColor: creator.colors.bannerBottom || '#A30E79' }">
|
||||
<div class="flex flex-col items-center lg:flex-row lg:items-center lg:justify-between">
|
||||
<!-- Profile Image Wrapper -->
|
||||
<div class="relative flex justify-center lg:w-auto lg:justify-start">
|
||||
|
||||
</div>
|
||||
<!--Banner & user info-->
|
||||
<div class="relative">
|
||||
<!--Banner-->
|
||||
<div>
|
||||
<img class=" w-full drop-shadow-[0_15px_10px_rgba(0,0,0,0.7)]" :src="creator.storedDataUrls.bannerPictureUrl || '/images/hutopymedia/banners/tutorialbanner.png'" alt="Profile Banner" style="max-height: 550px">
|
||||
</div>
|
||||
<!--User info -->
|
||||
<div class="absolute top-1/2 right-10 text-white z-30 transform -translate-y-1/2">
|
||||
<div class="text-white">
|
||||
<div class="text-2xl sm:text-3xl md:text-2xl lg:text-4xl xl:text-6xl font-bold">
|
||||
<p :style="{ color: creator.profileColors.accent }">{{ creator.firstName }}</p>
|
||||
</div>
|
||||
<div class="text-2xl sm:text-3xl md:text-2xl lg:text-4xl xl:text-6xl font-bold">
|
||||
<p :style="{ color: creator.profileColors.accent }">{{ creator.lastName }}</p>
|
||||
</div>
|
||||
<div class="text-1xl sm:text-xl md:text-lg lg:text-2xl xl:text-3xl">
|
||||
<p :style="{ color: creator.profileColors.accent }">{{ creator.occupation }}</p>
|
||||
<!-- Profile Image -->
|
||||
<div class="absolute lg:ml-72 transform -translate-y-1/2 lg:-translate-y-1/2 z-20">
|
||||
<img
|
||||
class="rounded-full border-solid border-2 z-20 lg:max-w-[200px] max-w-[200px] h-auto"
|
||||
:src="creator.images.logo"
|
||||
alt="Profile Picture"
|
||||
:style="{ borderColor: creator.colors.accent || '#A30E79', height: '150px'}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Info -->
|
||||
<div class="mt-2 flex flex-col items-center lg:items-start lg:ml-64">
|
||||
<div class="text-3xl font-bold text-center lg:text-left md:mt-24 lg:mt-0 sm:mt-24 mt-24 text-white cap ">
|
||||
<p class="capitalize ">{{ creator.name }} </p>
|
||||
<div class="text-lg text-white">{{ creator.subscriberCount }} Abonnés</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 mt-2">
|
||||
<subscribe-button :creator="creator"></subscribe-button>
|
||||
</div>
|
||||
|
||||
<donation-button :color-border="creator.colors.menu"
|
||||
:color-accent="creator.colors.accent"
|
||||
:creator-id="creator.id"
|
||||
:creator-name="creator.name"
|
||||
:creator-logo="creator.images.logo"
|
||||
iconColorClass="text-white"
|
||||
></donation-button>
|
||||
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
|
||||
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="flex flex-wrap items-center mt-2 sm:mt-8 md:mt-4 lg:mt-0 lg:ml-auto space-x-2 sm:space-x-4">
|
||||
|
||||
<publish-content-button :creator="creator"
|
||||
@content-posted="addContent"
|
||||
></publish-content-button>
|
||||
|
||||
<div class="text-white text-2xl">
|
||||
{{ creator.about.title }}
|
||||
</div>
|
||||
|
||||
<creator-about :creator="creator"
|
||||
></creator-about>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Button & image profile -->
|
||||
<div class="relative bottom-4 w-full">
|
||||
<div class="bg-gray-800 py-4 relative shadow-lg min-h-24" :style="{ backgroundColor: creator.profileColors.bannerBottom || '#A30E79' }">
|
||||
<div class="flex flex-col sm:flex-row items-center sm:justify-between">
|
||||
<img
|
||||
class="left-5 rounded-full border-solid border-2 sm:absolute sm:top-1/2 sm:transform sm:-translate-y-1/2 md:left-20 z-20"
|
||||
:src="creator.storedDataUrls.profilePictureUrl || '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png'"
|
||||
alt="Profile Picture"
|
||||
:style="{ borderColor: creator.profileColors.accent || '#A30E79', maxWidth: '250px', width: '100%' }"
|
||||
>
|
||||
<div v-if="creator.id === userStore.getCurrentUser().id" class="flex flex-wrap sm:flex-nowrap items-center mt-4 sm:mt-0 sm:ml-auto space-x-2 sm:space-x-4">
|
||||
<button
|
||||
class="flex items-center text-white transform transition-transform duration-200 hover:text-gray-300 hover:scale-125"
|
||||
@click="isDialogActive = true">
|
||||
<v-icon style="font-size: 35px; height: 35px; width: 55px;">mdi-text-box-plus-outline</v-icon>
|
||||
</button>
|
||||
<button class="text-white py-2 px-4 rounded"
|
||||
style="background-color: #333; transition: background-color 0.3s ease;"
|
||||
onmouseover="this.style.backgroundColor='#555';" onmouseout="this.style.backgroundColor='#333';">
|
||||
S'ABONNER
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center text-white transform transition-transform duration-200 hover:text-white hover:scale-125">
|
||||
<v-icon style="font-size: 35px; height: 35px; width: 55px;">mdi-comment-text</v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center text-white transform transition-transform duration-200 hover:text-gray-300 hover:scale-125">
|
||||
<v-icon style="font-size: 35px; height: 35px; width: 55px;">mdi-information</v-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Post-modale-->
|
||||
<v-dialog v-model="isDialogActive" max-width="500">
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card class="text-center rounded-xl">
|
||||
<div class="text-white p-4 rounded-t" :style="{backgroundColor: creator.profileColors.menu || '#A30E79'}">
|
||||
<h2>Publication</h2>
|
||||
</div>
|
||||
<v-card-text class="bg">
|
||||
<v-row class="justify-center mb-4">
|
||||
<v-col class="d-flex align-center justify-end">
|
||||
<v-btn :class="{'v-btn--active': !isArticle}" @click="togglePostType(false)">QUICKY</v-btn>
|
||||
</v-col>
|
||||
<v-col class="d-flex align-center justify-start">
|
||||
<v-btn :class="{'v-btn--active': isArticle}" @click="togglePostType(true)">ARTICLE</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-textarea label="Écrivez votre message ici..." v-model="message" outlined></v-textarea>
|
||||
<v-file-input
|
||||
label="Ajoutez des fichiers"
|
||||
v-model="files"
|
||||
outlined
|
||||
multiple
|
||||
dropzone
|
||||
placeholder="Glissez et déposez des fichiers ici ou cliquez pour sélectionner des fichiers"
|
||||
></v-file-input>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn color="primary" @click="publishPost">Publier</v-btn>
|
||||
<v-btn color="primary" @click="cancelPost">Annuler</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {defineProps, ref} from "vue";
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import CreatorAbout from "@/views/creators/CreatorAbout.vue";
|
||||
import SubscribeButton from "@/views/creators/SubscribeButton.vue";
|
||||
import PublishContentButton from "@/views/contents/PublishContentButton.vue";
|
||||
import DonationButton from "@/views/creators/DonationButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
creator: { type: Object, required: true },
|
||||
creator: {type: Object, required: true},
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const emits = defineEmits(['content-posted'])
|
||||
|
||||
const isDialogActive = ref(false);
|
||||
const message = ref('');
|
||||
const files = ref([]);
|
||||
const isArticle = ref(false);
|
||||
function addContent(content) {
|
||||
emits('content-posted', content)
|
||||
}
|
||||
|
||||
const publishPost = () => {
|
||||
// Logic to publish post
|
||||
console.log('Post published:', message.value, files.value);
|
||||
isDialogActive.value = false;
|
||||
resetPostForm();
|
||||
};
|
||||
function GetSocialsUrls() {
|
||||
|
||||
const cancelPost = () => {
|
||||
isDialogActive.value = false;
|
||||
resetPostForm();
|
||||
};
|
||||
const socials = [];
|
||||
|
||||
const resetPostForm = () => {
|
||||
message.value = '';
|
||||
files.value = [];
|
||||
};
|
||||
|
||||
const togglePostType = (article) => {
|
||||
isArticle.value = article;
|
||||
};
|
||||
|
||||
function GetActiveSocialNetworkUrls(){
|
||||
const socialNetworks = [];
|
||||
const userSocialNetworks = props.creator.socialNetworks;
|
||||
if (userSocialNetworks.facebookUrl !== ''){
|
||||
socialNetworks.push({icon: "mdi-facebook", url: props.creator.socialNetworks.facebookUrl})
|
||||
if (props.creator.socials.facebookUrl !== null) {
|
||||
socials.push({
|
||||
icon: "mdi-facebook",
|
||||
url: props.creator.socials.facebookUrl
|
||||
})
|
||||
}
|
||||
if (userSocialNetworks.facebookUrl !== ''){
|
||||
socialNetworks.push({icon: "mdi-twitter", url: props.creator.socialNetworks.xUrl})
|
||||
if (props.creator.socials.instagramUrl !== null) {
|
||||
socials.push({
|
||||
icon: "mdi-instagram",
|
||||
url: props.creator.socials.instagramUrl
|
||||
})
|
||||
}
|
||||
if (userSocialNetworks.instagramUrl !== ''){
|
||||
socialNetworks.push({icon: "mdi-instagram", url: props.creator.socialNetworks.instagramUrl})
|
||||
if (props.creator.socials.xUrl !== null) {
|
||||
socials.push({
|
||||
icon: "mdi-twitter",
|
||||
url: props.creator.socials.xUrl
|
||||
})
|
||||
}
|
||||
if (userSocialNetworks.tiktokUrl !== ''){
|
||||
socialNetworks.push({icon: "/images/hutopymedia/icons/white/tiktokwhite.png", url: props.creator.socialNetworks.tikTokUrl})
|
||||
if (props.creator.socials.linkedInUrl !== null) {
|
||||
socials.push({
|
||||
icon: 'mdi-linkedin',
|
||||
url: props.creator.socials.linkedInUrl
|
||||
})
|
||||
}
|
||||
if (userSocialNetworks.youtubeUrl !== ''){
|
||||
socialNetworks.push({icon: "mdi-youtube", url: props.creator.socialNetworks.youtubeUrl})
|
||||
if (props.creator.socials.tikTokUrl !== null) {
|
||||
socials.push({
|
||||
icon: '/images/socials/tiktok-white.png',
|
||||
url: props.creator.socials.tikTokUrl
|
||||
})
|
||||
}
|
||||
if (userSocialNetworks.yourWebsiteUrl !== ''){
|
||||
const websiteIconWithBackup = props.creator.storedDataUrls.websiteIconUrl === '' ? "mdi-web" : props.creator.storedDataUrls.websiteIconUrl;
|
||||
socialNetworks.push({icon: websiteIconWithBackup, url: props.creator.socialNetworks.yourWebsiteUrl})
|
||||
if (props.creator.socials.youtubeUrl !== null) {
|
||||
socials.push({
|
||||
icon: 'mdi-youtube',
|
||||
url: props.creator.socials.youtubeUrl
|
||||
})
|
||||
}
|
||||
if (props.creator.socials.redditUrl !== null) {
|
||||
socials.push({
|
||||
icon: 'mdi-reddit',
|
||||
url: props.creator.socials.redditUrl
|
||||
})
|
||||
}
|
||||
if (props.creator.socials.websiteUrl !== null) {
|
||||
socials.push({
|
||||
icon: 'mdi-web',
|
||||
url: props.creator.socials.websiteUrl
|
||||
})
|
||||
}
|
||||
|
||||
return socialNetworks;
|
||||
return socials;
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -8,7 +8,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
defineProps({
|
||||
creator: {
|
||||
|
||||
@@ -28,6 +28,5 @@
|
||||
<script setup>
|
||||
|
||||
import CreatorCard from "@/views/creators/CreatorCard.vue";
|
||||
import creators from "@/views/creators/creators.json";
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,46 +1,18 @@
|
||||
<template>
|
||||
<div v-if="creator && creator.id">
|
||||
|
||||
<creator-banner :creator="creator"></creator-banner>
|
||||
|
||||
<DonationPopup :creator-id="creator.id"></DonationPopup>
|
||||
|
||||
<div class="max-w-[1000px] mx-auto flex flex-row justify-center border-l-2 border-r-2 -mt-6 ">
|
||||
|
||||
<div class="w-full mt-20">
|
||||
|
||||
|
||||
|
||||
<v-card-text>
|
||||
<v-tabs-window v-model="tab">
|
||||
<v-tabs-window-item value="content">
|
||||
<div class="w-full h-full p-6">
|
||||
<ContentList v-if="creator.id"
|
||||
:creator-id="creator.id">
|
||||
</ContentList>
|
||||
</div>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="community">
|
||||
<div>
|
||||
<div class="border-b-2 p-6">
|
||||
<PostMessage v-if="creator.id"
|
||||
:content-id="creator.id">
|
||||
</PostMessage>
|
||||
</div>
|
||||
|
||||
<div class="border-b p-6">
|
||||
<h2 class="font-sans font-semibold">Commentaires</h2>
|
||||
<MessageList v-if="creator.id"
|
||||
:content-id="creator.id">
|
||||
</MessageList>
|
||||
</div>
|
||||
</div>
|
||||
</v-tabs-window-item>
|
||||
|
||||
</v-tabs-window>
|
||||
</v-card-text>
|
||||
<div class="max-w-[1250px] mx-auto">
|
||||
<creator-banner :creator="creator"
|
||||
@content-posted="contentPosted"
|
||||
></creator-banner>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[800px] mx-auto">
|
||||
<div class="w-full h-full mx-1">
|
||||
<content-list :creator-id="creator.id"
|
||||
:contents="contents"
|
||||
></content-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,58 +37,36 @@ import {watch, ref, onBeforeMount} from 'vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import CreatorBanner from "@/views/creators/CreatorBanner.vue";
|
||||
import MessageList from "@/views/messages/MessageList.vue";
|
||||
import ContentList from "@/views/contents/ContentList.vue";
|
||||
import PostMessage from "@/views/messages/PostMessage.vue";
|
||||
import DonationPopup from "@/views/main/DonationPopup.vue";
|
||||
|
||||
const client = useClient();
|
||||
const route = useRoute();
|
||||
const creator = ref(null)
|
||||
const loading = ref(true)
|
||||
const contents = ref([])
|
||||
|
||||
const creator = ref(null);
|
||||
const loading = ref(true);
|
||||
const tab = ref();
|
||||
const client = useClient()
|
||||
const route = useRoute()
|
||||
|
||||
onBeforeMount(async() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 1500);
|
||||
await fetchCreatorData(route.params.creator);
|
||||
});
|
||||
function contentPosted(content) {
|
||||
contents.value.unshift(content)
|
||||
}
|
||||
|
||||
onBeforeMount(async () => await fetchCreatorData(route.params.creator))
|
||||
|
||||
watch(
|
||||
() => route.params.creator,
|
||||
async () => {
|
||||
loading.value = true;
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
await fetchCreatorData(route.params.creator);
|
||||
}
|
||||
);
|
||||
async () => await fetchCreatorData(route.params.creator)
|
||||
)
|
||||
|
||||
const fetchCreatorData = async (creatorAlias) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await client.get(`/api/creators/@${creatorAlias}`)
|
||||
creator.value = response.data;
|
||||
console.log('loaded the following creator from api');
|
||||
console.table(creator.value.id);
|
||||
creator.value = response.data
|
||||
} catch (error) {
|
||||
console.error(`Error fetching content: ${error}`);
|
||||
console.error(`Error fetching content: ${error}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.v-btn--active {
|
||||
background-color: #1976D2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
152
src/views/creators/DonationButton.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<v-btn 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 ${colorBorder}` }">
|
||||
<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="creatorLogo"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="40"
|
||||
height="40"
|
||||
:style="{ border: `2px solid ${colorAccent}` }">
|
||||
<div class="capitalize px-2 text-2xl">{{ creatorName }}</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="tipAmount"
|
||||
class="p-2"
|
||||
label="Montant"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
></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: colorBorder, color: colorBorder }"
|
||||
@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-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn block class="ma-auto" style="width: 200px;" text @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 {computed, onMounted, ref} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
colorBorder: {type: String, required: true},
|
||||
colorAccent: {type: String, required: true},
|
||||
creatorId: {type: String, required: true},
|
||||
creatorName: {type: String, required: true},
|
||||
creatorLogo: {type: String, required: true},
|
||||
iconColorClass: {type: String, default: 'text-black'}
|
||||
});
|
||||
|
||||
const colorBorder = computed(() => props.colorBorder)
|
||||
const colorAccent = computed(() => props.colorAccent)
|
||||
const creatorId = computed(() => props.creatorId)
|
||||
const creatorName = computed(() => props.creatorName)
|
||||
const creatorLogo = computed(() => props.creatorLogo)
|
||||
|
||||
const donationModal = ref(false);
|
||||
|
||||
function openDonationDialog() {
|
||||
donationModal.value = true
|
||||
}
|
||||
|
||||
function closeDonationDialog() {
|
||||
donationModal.value = false
|
||||
}
|
||||
|
||||
|
||||
const isPaymentDialogActive = ref(false);
|
||||
|
||||
const tipAmount = ref(0);
|
||||
const tipMessage = ref("");
|
||||
|
||||
let stripe = null;
|
||||
let 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() {
|
||||
const client = useClient()
|
||||
let clientSecret = await client.post('/api/Stripe', {
|
||||
amount: (tipAmount.value * 100),
|
||||
tipMessage: tipMessage.value,
|
||||
creatorId: props.creatorId
|
||||
|
||||
});
|
||||
|
||||
let secret = clientSecret["data"];
|
||||
return secret;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
isPaymentDialogActive.value = false;
|
||||
if (checkout) {
|
||||
checkout.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async function goPay() {
|
||||
isPaymentDialogActive.value = true;
|
||||
|
||||
checkout = await stripe.initEmbeddedCheckout({
|
||||
fetchClientSecret,
|
||||
});
|
||||
|
||||
await checkout.mount('#checkout');
|
||||
}
|
||||
</script>
|
||||
85
src/views/creators/SubscribeButton.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
import {useSubscriptionStore} from "@/stores/subscriptionStore.js";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
creator: {type: Object, required: true},
|
||||
});
|
||||
|
||||
const subscriptionStore = useSubscriptionStore();
|
||||
|
||||
const isSubscribe = computed(() => !subscriptionStore.isSubscribeTo(props.creator.id));
|
||||
|
||||
function subscribeToCreator() {
|
||||
subscriptionStore.subscribeTo(props.creator.id);
|
||||
}
|
||||
|
||||
// Référence pour contrôler l'affichage du modal
|
||||
const showUnsubscribeModal = ref(false);
|
||||
|
||||
function unsubscribeFromCreator() {
|
||||
subscriptionStore.unsubscribeFrom(props.creator.id);
|
||||
// Fermer le modal après désabonnement
|
||||
showUnsubscribeModal.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="isSubscribe">
|
||||
<v-btn
|
||||
class="action mr-4"
|
||||
variant="outlined"
|
||||
@click="subscribeToCreator"
|
||||
color="white"
|
||||
>
|
||||
S'ABONNER
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-btn
|
||||
class="action mr-4"
|
||||
variant="outlined"
|
||||
@click="showUnsubscribeModal = true"
|
||||
style="transition: background-color 0.3s ease;"
|
||||
color="white"
|
||||
>
|
||||
SE DESABONNER
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-dialog v-model="showUnsubscribeModal" max-width="500">
|
||||
<v-card class="text-center rounded-xl"
|
||||
:style="{
|
||||
border: `3px solid ${creator.colors.menu}`
|
||||
}">
|
||||
|
||||
<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="background-color: rgba(255, 255, 255, 0.1); color: rgba(0, 0, 0, 0.4);"
|
||||
@click="unsubscribeFromCreator">Oui
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="secondary" class="flex-grow-1"
|
||||
:style="{ borderColor: creator.colors.menu, color: creator.colors.menu }" variant="outlined"
|
||||
@click="showUnsubscribeModal = false "> <div :style="{color: creator.colors.menu}">non</div>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.action {
|
||||
@apply py-2 px-4 rounded;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
</style>
|
||||
35
src/views/creators/SubscriptionList.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<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 :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>
|
||||
<slot>
|
||||
<span>Placeholder when there are no subscriptions</span>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
@@ -1,51 +0,0 @@
|
||||
[
|
||||
{
|
||||
"ìd": 1,
|
||||
"name": "Hutopy",
|
||||
"title": "Page officielle",
|
||||
"description": "Site officiel pour Hutopy. Venez-nous-y retrouver avec tous vos fans!",
|
||||
"imageUrl": "/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png",
|
||||
"routerLink": "@hutopy",
|
||||
"about": "Notre mission chez Hutopy est d'aider les gens à s'épanouir en leur proposant un environnement de partage unique qui va leur permettre de se surpasser. Nous travaillons étroitement avec la communauté d'Hutopy afin de concentrer notre développement sur ce que vous demandez et sur vos besoins, afin d'offrir les meilleurs outils. Le tout en gardant à l'esprit l'aspect humain de chacun. Il est important pour nous de contribuer à créer un monde meilleur.",
|
||||
"contacts": {
|
||||
"twitter": "https://twitter.com/Hutopyinc",
|
||||
"facebook": "https://www.facebook.com/Hutopy",
|
||||
"instagram": "https://www.instagram.com/hutopy.inc/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "L'effet",
|
||||
"description": "Fondation",
|
||||
"imageUrl": "/images/usersmedia/leffet/profilepictures/leffetProfile01.png",
|
||||
"routerLink": "@leffet"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Guillaume M",
|
||||
"description": "Créateur de contenus",
|
||||
"imageUrl": "/images/usersmedia/guillaumeMousseau/profilepictures/profileGuillaumeMousseau01.png",
|
||||
"routerLink": "@guillaumeaime"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Chloé Beaugrand",
|
||||
"description": "Spécialiste en médias sociaux",
|
||||
"imageUrl": "/images/usersmedia/chloebeaugrand/profilepictures/profileChloeBeaugrand01.png",
|
||||
"routerLink": "@chloebeaugrand"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Mathieu Caron",
|
||||
"description": "Entrevue Atypique",
|
||||
"imageUrl": "/images/usersmedia/mathieuCaron/profilepictures/profileMathieuCaron01.png",
|
||||
"routerLink": "@mathieuCaron"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "ARPS",
|
||||
"description": "Agence créative",
|
||||
"imageUrl": "/images/usersmedia/ARPS/profilepictures/profileARPS.png",
|
||||
"routerLink": "@ARPS"
|
||||
}
|
||||
]
|
||||
@@ -190,7 +190,3 @@
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import 'src/cssstyle/documentation.css';
|
||||
</style>
|
||||
|
||||
35
src/views/explorer/ExplorerCard.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="bg-white shadow-lg rounded-lg overflow-hidden transition-all duration-300 ease-in-out py4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="text-lg font-bold p-3">{{ video.title }}</h3>
|
||||
<v-btn icon class="ml-2" variant="plain">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<img :src="video.thumbnail" alt="video thumbnail" class="w-full ">
|
||||
<div class="p-3">
|
||||
<p class="text-gray-600 mb-4">{{ video.description }}</p>
|
||||
<div class="flex items-center">
|
||||
<img :src="video.profileImage" alt="profile image" class="w-10 h-10 rounded-full">
|
||||
<div class="ml-4">
|
||||
<h4 class="text-md font-bold">{{ video.name }}</h4>
|
||||
<div class="text-gray-600 text-sm">
|
||||
<span>{{ video.vues }} vues</span> -
|
||||
<span>{{ video.timeAgo }} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
video: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
227
src/views/explorer/explorer.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import videosData from './videos.json';
|
||||
import ExplorerCard from '@/views/explorer/ExplorerCard.vue';
|
||||
|
||||
// Données
|
||||
const mostViewedVideos = videosData.mostViewedVideos;
|
||||
const mostBoughtVideos = videosData.mostBoughtVideos;
|
||||
|
||||
// État pour contrôler la plage actuelle des vidéos affichées
|
||||
const currentRangeViewed = ref(0);
|
||||
const currentRangeBought = ref(0);
|
||||
|
||||
const videosPerRowViewed = ref(4); // Nombre initial de vidéos par ligne pour Most Viewed
|
||||
const videosPerRowBought = ref(4); // Nombre initial de vidéos par ligne pour Most Bought
|
||||
|
||||
const selectedTimeRangeViewed = ref('24h'); // Plage de temps sélectionnée pour Most Viewed
|
||||
const selectedTimeRangeBought = ref('24h'); // Plage de temps sélectionnée pour Most Bought
|
||||
|
||||
// Fonction pour passer à l'ensemble suivant de vidéos (Most Viewed)
|
||||
const nextRowViewed = () => {
|
||||
if (currentRangeViewed.value + videosPerRowViewed.value < mostViewedVideos.length) {
|
||||
currentRangeViewed.value += videosPerRowViewed.value;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour revenir à l'ensemble précédent de vidéos (Most Viewed)
|
||||
const previousRowViewed = () => {
|
||||
if (currentRangeViewed.value - videosPerRowViewed.value >= 0) {
|
||||
currentRangeViewed.value -= videosPerRowViewed.value;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour passer à l'ensemble suivant de vidéos (Most Bought)
|
||||
const nextRowBought = () => {
|
||||
if (currentRangeBought.value + videosPerRowBought.value < mostBoughtVideos.length) {
|
||||
currentRangeBought.value += videosPerRowBought.value;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour revenir à l'ensemble précédent de vidéos (Most Bought)
|
||||
const previousRowBought = () => {
|
||||
if (currentRangeBought.value - videosPerRowBought.value >= 0) {
|
||||
currentRangeBought.value -= videosPerRowBought.value;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour basculer entre l'affichage de 4 et 8 vidéos (Most Viewed)
|
||||
const toggleVideoDisplayViewed = () => {
|
||||
if (videosPerRowViewed.value === 4) {
|
||||
if (currentRangeViewed.value + videosPerRowViewed.value >= mostViewedVideos.length) {
|
||||
currentRangeViewed.value = Math.max(mostViewedVideos.length - 8, 0);
|
||||
}
|
||||
videosPerRowViewed.value = 8;
|
||||
} else {
|
||||
videosPerRowViewed.value = 4;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour basculer entre l'affichage de 4 et 8 vidéos (Most Bought)
|
||||
const toggleVideoDisplayBought = () => {
|
||||
if (videosPerRowBought.value === 4) {
|
||||
if (currentRangeBought.value + videosPerRowBought.value >= mostBoughtVideos.length) {
|
||||
currentRangeBought.value = Math.max(mostBoughtVideos.length - 8, 0);
|
||||
}
|
||||
videosPerRowBought.value = 8;
|
||||
} else {
|
||||
videosPerRowBought.value = 4;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour changer la plage de temps sélectionnée pour Most Viewed
|
||||
const changeTimeRangeViewed = (range) => {
|
||||
selectedTimeRangeViewed.value = range;
|
||||
currentRangeViewed.value = 0; // Réinitialiser la plage de vidéos affichées (Most Viewed)
|
||||
// Logique pour filtrer les vidéos en fonction de la plage de temps sélectionnée
|
||||
// À implémenter si nécessaire
|
||||
};
|
||||
|
||||
// Fonction pour changer la plage de temps sélectionnée pour Most Bought
|
||||
const changeTimeRangeBought = (range) => {
|
||||
selectedTimeRangeBought.value = range;
|
||||
currentRangeBought.value = 0; // Réinitialiser la plage de vidéos affichées (Most Bought)
|
||||
// Logique pour filtrer les vidéos en fonction de la plage de temps sélectionnée
|
||||
// À implémenter si nécessaire
|
||||
};
|
||||
|
||||
// Computed property pour le changement d'icône du chevron (Most Viewed)
|
||||
const chevronIconViewed = computed(() => {
|
||||
return videosPerRowViewed.value === 4 ? 'mdi-chevron-down' : 'mdi-chevron-up';
|
||||
});
|
||||
|
||||
// Computed property pour le changement d'icône du chevron (Most Bought)
|
||||
const chevronIconBought = computed(() => {
|
||||
return videosPerRowBought.value === 4 ? 'mdi-chevron-down' : 'mdi-chevron-up';
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto py-4">
|
||||
<!-- Section Most Viewed avec boutons de sélection de la plage de temps -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold">Most Viewed</h2>
|
||||
<div class="flex space-x-4">
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeViewed('24h')" :class="{'v-btn--active': selectedTimeRangeViewed === '24h'}">
|
||||
<v-icon left>mdi-timer</v-icon> 24h
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeViewed('72h')" :class="{'v-btn--active': selectedTimeRangeViewed === '72h'}">
|
||||
<v-icon left>mdi-timer-sand</v-icon> 72h
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeViewed('week')" :class="{'v-btn--active': selectedTimeRangeViewed === 'week'}">
|
||||
<v-icon left>mdi-calendar-week</v-icon> Semaine
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeViewed('month')" :class="{'v-btn--active': selectedTimeRangeViewed === 'month'}">
|
||||
<v-icon left>mdi-calendar-month</v-icon> Mois
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des vidéos les plus vues -->
|
||||
<div class="flex flex-row">
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 mb-8">
|
||||
<ExplorerCard
|
||||
v-for="(video, index) in mostViewedVideos.slice(currentRangeViewed, currentRangeViewed + videosPerRowViewed)"
|
||||
:key="video.id"
|
||||
:video="video"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center px-2">
|
||||
<!-- Bouton pour naviguer vers l'ensemble précédent de vidéos (Most Viewed) -->
|
||||
<v-btn @click="previousRowViewed" :disabled="currentRangeViewed === 0" class="mb-2">
|
||||
<v-icon>mdi-arrow-up-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
<!-- Bouton pour naviguer vers l'ensemble suivant de vidéos (Most Viewed) -->
|
||||
<v-btn @click="nextRowViewed" :disabled="currentRangeViewed + videosPerRowViewed.value >= mostViewedVideos.length">
|
||||
<v-icon>mdi-arrow-down-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chevron pour basculer entre l'affichage de 4 et 8 vidéos (Most Viewed) -->
|
||||
<div class="flex justify-center mt-4">
|
||||
<v-btn icon @click="toggleVideoDisplayViewed">
|
||||
<v-icon>{{ chevronIconViewed }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Ligne de séparation -->
|
||||
<hr class="my-8"/>
|
||||
|
||||
<!-- Section Most Bought avec boutons de sélection de la plage de temps -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold">Most Bought</h2>
|
||||
<div class="flex space-x-4">
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeBought('24h')" :class="{'v-btn--active': selectedTimeRangeBought === '24h'}">
|
||||
<v-icon left>mdi-timer</v-icon> 24h
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeBought('72h')" :class="{'v-btn--active': selectedTimeRangeBought === '72h'}">
|
||||
<v-icon left>mdi-timer-sand</v-icon> 72h
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeBought('week')" :class="{'v-btn--active': selectedTimeRangeBought === 'week'}">
|
||||
<v-icon left>mdi-calendar-week</v-icon> Semaine
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" small @click="changeTimeRangeBought('month')" :class="{'v-btn--active': selectedTimeRangeBought === 'month'}">
|
||||
<v-icon left>mdi-calendar-month</v-icon> Mois
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des vidéos les plus achetées -->
|
||||
<div class="flex flex-row">
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 mb-8">
|
||||
<ExplorerCard
|
||||
v-for="(video, index) in mostBoughtVideos.slice(currentRangeBought, currentRangeBought + videosPerRowBought)"
|
||||
:key="video.id"
|
||||
:video="video"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center px-2">
|
||||
<!-- Bouton pour naviguer vers l'ensemble précédent de vidéos (Most Bought) -->
|
||||
<v-btn @click="previousRowBought" :disabled="currentRangeBought === 0" class="mb-2">
|
||||
<v-icon>mdi-arrow-up-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
<!-- Bouton pour naviguer vers l'ensemble suivant de vidéos (Most Bought) -->
|
||||
<v-btn @click="nextRowBought" :disabled="currentRangeBought + videosPerRowBought.value >= mostBoughtVideos.length">
|
||||
<v-icon>mdi-arrow-down-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chevron pour basculer entre l'affichage de 4 et 8 vidéos (Most Bought) -->
|
||||
<div class="flex justify-center mt-4">
|
||||
<v-btn icon @click="toggleVideoDisplayBought">
|
||||
<v-icon>{{ chevronIconBought }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.v-btn--active {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.v-btn--active.v-btn--text {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.v-btn--text:hover {
|
||||
background-color: rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
410
src/views/explorer/videos.json
Normal file
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"mostLikedVideos": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Top 10 Football Goals",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A compilation of the best goals in football history.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Alex Martin",
|
||||
"vues": 5000,
|
||||
"timeAgo": "3 hours"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Basketball Dunk Contest Highlights",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Watch the best dunks from the latest dunk contest.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Jordan Lee",
|
||||
"vues": 4500,
|
||||
"timeAgo": "5 hours"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Epic Tennis Rallies",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A look at some of the most intense rallies in tennis.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Serena West",
|
||||
"vues": 4700,
|
||||
"timeAgo": "1 day"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Marathon Motivation",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Get inspired by these marathon runners.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Eliud Kip",
|
||||
"vues": 3800,
|
||||
"timeAgo": "2 days"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Best Volleyball Spikes",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Watch these powerful volleyball spikes in action.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Misty May",
|
||||
"vues": 4200,
|
||||
"timeAgo": "3 days"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Cycling Through The Alps",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Experience the thrill of cycling in the Alps.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Lance Ryder",
|
||||
"vues": 3900,
|
||||
"timeAgo": "4 days"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Cricket's Greatest Hits",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A look at some of the most memorable moments in cricket.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Sachin Kumar",
|
||||
"vues": 4400,
|
||||
"timeAgo": "5 days"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Top MMA Knockouts",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "The most shocking knockouts in MMA history.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Conor Mac",
|
||||
"vues": 5000,
|
||||
"timeAgo": "6 days"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "F1 Racing Highlights",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A thrilling recap of the latest Formula 1 races.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Lewis Ham",
|
||||
"vues": 4800,
|
||||
"timeAgo": "1 week"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "Golf's Most Amazing Shots",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Witness the most incredible shots in golf.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Tiger Woods",
|
||||
"vues": 5300,
|
||||
"timeAgo": "1 week"
|
||||
}
|
||||
],
|
||||
"mostBoughtVideos": [
|
||||
{
|
||||
"id": 11,
|
||||
"title": "Football Training Drills",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Master the best football drills for every position.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Pep Guardiola",
|
||||
"vues": 6000,
|
||||
"timeAgo": "2 hours"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "Advanced Basketball Techniques",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Learn advanced techniques to improve your game.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Kobe Bryant",
|
||||
"vues": 5500,
|
||||
"timeAgo": "4 hours"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"title": "Perfecting Your Tennis Serve",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Tips and tricks to perfect your tennis serve.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Roger Federer",
|
||||
"vues": 5100,
|
||||
"timeAgo": "6 hours"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"title": "Marathon Nutrition Guide",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Essential nutrition tips for marathon runners.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Meb Keflezighi",
|
||||
"vues": 4800,
|
||||
"timeAgo": "1 day"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Volleyball Defense Mastery",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Become a defensive powerhouse in volleyball.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Kerri Walsh",
|
||||
"vues": 4700,
|
||||
"timeAgo": "2 days"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"title": "Ultimate Cycling Techniques",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Improve your cycling skills with these techniques.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Chris Froome",
|
||||
"vues": 4600,
|
||||
"timeAgo": "3 days"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"title": "Mastering Spin Bowling",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Learn the art of spin bowling in cricket.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Shane Warne",
|
||||
"vues": 5200,
|
||||
"timeAgo": "4 days"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"title": "MMA Grappling Techniques",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Dominate the ground game with these techniques.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Khabib Nur",
|
||||
"vues": 5300,
|
||||
"timeAgo": "5 days"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"title": "F1 Pit Stop Secrets",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Learn the strategies behind a perfect F1 pit stop.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Sebastian Vettel",
|
||||
"vues": 5400,
|
||||
"timeAgo": "6 days"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "Golf Putting Techniques",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Improve your putting with these expert tips.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Phil Mickelson",
|
||||
"vues": 5500,
|
||||
"timeAgo": "1 week"
|
||||
}
|
||||
],
|
||||
"mostSharedVideos": [
|
||||
{
|
||||
"id": 21,
|
||||
"title": "Soccer Match Replays",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Watch full replays of the latest soccer matches.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Cristiano Ronaldo",
|
||||
"vues": 6000,
|
||||
"timeAgo": "1 hour"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"title": "Basketball Trick Shots",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "The most incredible trick shots in basketball.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Steph Curry",
|
||||
"vues": 5900,
|
||||
"timeAgo": "3 hours"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"title": "Tennis Grand Slam Finals",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Relive the excitement of Grand Slam tennis finals.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Novak Djokovic",
|
||||
"vues": 5800,
|
||||
"timeAgo": "5 hours"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"title": "Running Tips for Beginners",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Get started with running with these beginner tips.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Usain Bolt",
|
||||
"vues": 5700,
|
||||
"timeAgo": "1 day"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"title": "Beach Volleyball Highlights",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Catch the best moments from beach volleyball matches.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "April Ross",
|
||||
"vues": 5600,
|
||||
"timeAgo": "2 days"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"title": "Mountain Biking Adventures",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Experience the thrill of mountain biking.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Danny MacAskill",
|
||||
"vues": 5500,
|
||||
"timeAgo": "3 days"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"title": "Cricket World Cup Highlights",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "The most thrilling moments from the Cricket World Cup.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Virat Kohli",
|
||||
"vues": 5400,
|
||||
"timeAgo": "4 days"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"title": "MMA Fight Replays",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Watch full replays of the most intense MMA fights.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Anderson Silva",
|
||||
"vues": 5300,
|
||||
"timeAgo": "5 days"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"title": "F1 Crash Compilation",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A compilation of the most shocking F1 crashes.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Michael Schumacher",
|
||||
"vues": 5200,
|
||||
"timeAgo": "6 days"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"title": "Golf Tips for Beginners",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Essential tips for those new to golf.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Jack Nicklaus",
|
||||
"vues": 5100,
|
||||
"timeAgo": "1 week"
|
||||
}
|
||||
],
|
||||
"mostViewedVideos": [
|
||||
{
|
||||
"id": 31,
|
||||
"title": "Soccer's Greatest Goals",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A look at the greatest goals ever scored in soccer.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Lionel Messi",
|
||||
"vues": 7000,
|
||||
"timeAgo": "30 minutes"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"title": "Best of Basketball Finals",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Relive the best moments from recent basketball finals.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "LeBron James",
|
||||
"vues": 6900,
|
||||
"timeAgo": "1 hour"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"title": "Tennis Serve Speed Records",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Check out the fastest serves ever recorded in tennis.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Andy Roddick",
|
||||
"vues": 6800,
|
||||
"timeAgo": "2 hours"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"title": "Running in Extreme Weather",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Tips for running in various weather conditions.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Mo Farah",
|
||||
"vues": 6700,
|
||||
"timeAgo": "3 hours"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"title": "Volleyball Team Strategies",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Learn advanced team strategies for volleyball.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Giba Godoy",
|
||||
"vues": 6600,
|
||||
"timeAgo": "4 hours"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"title": "Cycling Gear Tips",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "A guide to choosing the best cycling gear.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Peter Sagan",
|
||||
"vues": 6500,
|
||||
"timeAgo": "1 day"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"title": "Cricket Batting Techniques",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Improve your batting with these cricket techniques.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Brian Lara",
|
||||
"vues": 6400,
|
||||
"timeAgo": "2 days"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"title": "MMA Striking Combinations",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Learn effective striking combinations in MMA.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Georges St-Pierre",
|
||||
"vues": 6300,
|
||||
"timeAgo": "3 days"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"title": "F1 Overtakes Compilation",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "The most exciting overtakes in F1 history.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Ayrton Senna",
|
||||
"vues": 6200,
|
||||
"timeAgo": "4 days"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"title": "Golf Swing Analysis",
|
||||
"thumbnail": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"description": "Breakdown of the best golf swings.",
|
||||
"profileImage": "images/hutopymedia/thumbnail/ThumbnailImage.png",
|
||||
"name": "Rory McIlroy",
|
||||
"vues": 6100,
|
||||
"timeAgo": "5 days"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<div class="py-8" ref="container">
|
||||
<div class="text-center text-2xl py-4 border-t-4">À propos de vous</div>
|
||||
<div class="px-5 relative">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Prénom</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.firstName" label="Prénom" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Nom</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.lastName" label="Nom" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Occupation</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.occupation" label="Occupation" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Courriel</label>
|
||||
</div>
|
||||
<v-text-field disabled v-model="user.email" label="Courriel" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Téléphone</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.phoneNumber" label="Téléphone" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Date de naissance (JJ/MM/AAAA)</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.birthDate" label="Date de naissance" variant="outlined"
|
||||
:rules="[dateRule]"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Pays</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.country" label="Pays" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Ville</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.city" label="Ville" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Adresse</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.address" label="Adresse" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Titre A propos</label>
|
||||
</div>
|
||||
<v-text-field v-model="user.about" label="Titre A propos" variant="outlined"></v-text-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Description</label>
|
||||
</div>
|
||||
<v-textarea v-model="user.description" label="Description" variant="outlined"></v-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps } from 'vue';
|
||||
import MyUserModel from "@/models/myUserModel.js";
|
||||
|
||||
const props = defineProps({
|
||||
user: { type: MyUserModel },
|
||||
});
|
||||
|
||||
const dateRule = value => {
|
||||
const datePattern = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/(19|20)\d{2}$/;
|
||||
return datePattern.test(value) || 'Format de date invalide (JJ/MM/AAAA)';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Votre style ici */
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div class="mt-28">
|
||||
|
||||
<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-4xl mb-4">
|
||||
<div class="md:flex">
|
||||
<div class="md:flex-shrink-0">
|
||||
<img class="h-48 w-full object-cover md:h-full md:w-48" src="/images/usersmedia/HutopyProfile/banners/banner01.png" alt="Image">
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">Nom du réseau social</div>
|
||||
<p class="mt-2 text-gray-500">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>
|
||||
<p class="mt-2 text-gray-500">Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-4xl mb-4">
|
||||
<div class="md:flex">
|
||||
<div class="md:flex-shrink-0">
|
||||
<img class="h-48 w-full object-cover md:h-full md:w-48" src="/images/usersmedia/HutopyProfile/banners/banner01.png" alt="Image">
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">Nom du réseau social</div>
|
||||
<p class="mt-2 text-gray-500">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>
|
||||
<p class="mt-2 text-gray-500">Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-4xl mb-4">
|
||||
<div class="md:flex">
|
||||
<div class="md:flex-shrink-0">
|
||||
<img class="h-48 w-full object-cover md:h-full md:w-48" src="/images/usersmedia/HutopyProfile/banners/banner01.png" alt="Image">
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">Nom du réseau social</div>
|
||||
<p class="mt-2 text-gray-500">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>
|
||||
<p class="mt-2 text-gray-500">Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<div class="fixed z-50 bottom-10 right-10 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 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
|
||||
@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 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, defineProps} from 'vue';
|
||||
import {ref, onMounted, onUnmounted} from 'vue';
|
||||
import StripePayment from "@/views/StripePayment.vue";
|
||||
|
||||
const showPopup = ref(false);
|
||||
@@ -35,7 +35,7 @@ const popup = ref(null);
|
||||
const popupButton = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
creatorId: { type: String, required: true },
|
||||
creatorId: {type: String, required: true},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<v-app-bar-nav-icon @click.stop="toggleSidebar">
|
||||
<v-app-bar-nav-icon @click.stop="sideBarStore.toggle()">
|
||||
</v-app-bar-nav-icon>
|
||||
|
||||
<RouterLink to="/" class="hidden md:block">
|
||||
<RouterLink to="/">
|
||||
<v-img
|
||||
src="/medias/hutopy.png"
|
||||
ref="popupButtonRef"
|
||||
@@ -16,16 +16,36 @@
|
||||
></v-img>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/" class="block md:hidden">
|
||||
<v-img
|
||||
src="/images/hutopymedia/icons/logohutopy.png"
|
||||
ref="popupButtonRef"
|
||||
alt="Hutopy Logo"
|
||||
class="mr-2 h-10 w-10"
|
||||
></v-img>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- Spacer to push the Explorer button to the center -->
|
||||
<div class="flex-grow"></div>
|
||||
|
||||
|
||||
<!-- FEED -->
|
||||
<v-btn
|
||||
variant="plain"
|
||||
icon
|
||||
class="flex flex-row justify-center"
|
||||
@click="explorerHandler"
|
||||
>
|
||||
<v-icon>mdi-television</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Explorer Button -->
|
||||
<v-btn
|
||||
variant="plain"
|
||||
icon
|
||||
class="flex flex-row justify-center"
|
||||
@click="explorerHandler"
|
||||
>
|
||||
<v-icon>mdi-earth</v-icon>
|
||||
</v-btn>
|
||||
|
||||
|
||||
<!-- Spacer to keep the search bar to the right -->
|
||||
<div class="flex-grow"></div>
|
||||
|
||||
<div class="flex items-center ml-auto space-x-4 search-container">
|
||||
<template v-if="showSearch">
|
||||
<v-text-field
|
||||
@@ -53,80 +73,79 @@
|
||||
<div class="text-center">
|
||||
<v-menu open-on-hover>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" v-bind="props" class=" d-flex align-center text-capital-none">
|
||||
<span class="normal-case max-w-xs hidden md:block">
|
||||
{{ currentUserName }}
|
||||
</span>
|
||||
<div v-bind="props" class="flex align-center font-sans py-1 px-4 rounded-lg hover:bg-gray-100">
|
||||
<span class="max-w-xs hidden md:block">
|
||||
{{ userStore.alias }}
|
||||
</span>
|
||||
<img
|
||||
:src="currentUser.storedDataUrls.profilePictureUrl"
|
||||
:src="userStore.portraitUrl"
|
||||
alt="Profile Image"
|
||||
@error="handleProfilePictureError"
|
||||
class="ml-2 rounded-full" style="width: 32px; height: 32px;">
|
||||
</v-btn>
|
||||
class="ml-2 rounded-full"
|
||||
width="32"
|
||||
height="32">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-list min-width="200px" class=" align-center mt-3 left-3">
|
||||
<div v-if="currentUser.userName === 'Anonyme'">
|
||||
<v-list min-width="200px" class="align-center mt-3 left-3">
|
||||
|
||||
<template v-if="!authStore.isAuthenticated">
|
||||
<v-list-item class="nav-button">
|
||||
<v-list-item-title>
|
||||
<v-btn to="/login" class="w-100 " variant="plain">Connexion</v-btn>
|
||||
<v-btn to="/login" class="w-100" variant="plain">Connexion</v-btn>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="currentUser.userName !== 'Anonyme'">
|
||||
<v-list-item class="nav-button">
|
||||
<router-link :to="`/@${currentUserName}`">
|
||||
<v-btn class="w-100 " variant="plain"> {{ currentUserName }}</v-btn>
|
||||
<template v-else>
|
||||
<v-list-item v-if="userStore.creator && Object.keys(userStore.creator).length > 0" class="nav-button">
|
||||
<router-link :to="`/@${userStore.creator.name}`">
|
||||
<v-btn class="w-100" variant="plain">@{{ userStore.creator.name }}</v-btn>
|
||||
</router-link>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="!userStore.hasCreator" class="nav-button">
|
||||
<router-link to="/profile">
|
||||
<v-btn class="w-100" variant="plain">Activer votre page</v-btn>
|
||||
</router-link>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="nav-button">
|
||||
<v-list-item-title >
|
||||
<v-btn to="/profile" class="w-100 " variant="plain">Mon profil</v-btn>
|
||||
<v-list-item-title>
|
||||
<v-btn to="/profile" class="w-100" variant="plain">Mon profil</v-btn>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item class="nav-button">
|
||||
<v-list-item-title>
|
||||
<v-btn to="/wallet" class="w-100 " variant="plain"> Portefeuille</v-btn>
|
||||
<v-btn to="/wallet" class="w-100" variant="plain">Portefeuille</v-btn>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item class="nav-button">
|
||||
<v-list-item-title>
|
||||
<v-btn @click="logout" to="/wallet" class="w-100 " variant="plain"> Déconnexion</v-btn>
|
||||
<v-btn @click="authStore.logout" class="w-100" variant="plain">Déconnexion</v-btn>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onBeforeUnmount, onBeforeMount, watch, reactive} from "vue";
|
||||
import {eventBus} from '@/eventBus.js';
|
||||
import {ref, onBeforeUnmount, onBeforeMount} from "vue";
|
||||
import {useRouter} from 'vue-router';
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import MyUserModel from "@/models/myUserModel.js";
|
||||
import {useSideBarStore} from '@/stores/sideBarStore.js';
|
||||
import {useUserStore} from "@/stores/userStore.js";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
const sideBarStore = useSideBarStore()
|
||||
|
||||
const router = useRouter();
|
||||
const currentUserName = ref("Anonyme");
|
||||
const searchQuery = ref("");
|
||||
const showSearch = ref(false);
|
||||
let currentUser = reactive(MyUserModel.getDefaultUser());
|
||||
const userStore = useUserStore();
|
||||
const backupProfilePictureUrl = "/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png"
|
||||
|
||||
const handleProfilePictureError = (event) => {
|
||||
event.target.src = backupProfilePictureUrl;
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
eventBus.value.toggleSidebar();
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
const query = searchQuery.value.trim();
|
||||
@@ -146,43 +165,25 @@ const toggleSearch = () => {
|
||||
showSearch.value = !showSearch.value;
|
||||
};
|
||||
|
||||
const explorerHandler = () => {
|
||||
router.push('/explorer');
|
||||
};
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (!event.target.closest('.search-container')) {
|
||||
showSearch.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToMessages = () => {
|
||||
router.push('/messages');
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('jwt');
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
onBeforeMount( () => {
|
||||
currentUser = userStore.getCurrentUser();
|
||||
currentUserName.value = currentUser.userName;
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
onBeforeMount(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
});
|
||||
|
||||
// Watch the user state to get it again if needed.
|
||||
watch(
|
||||
() => userStore.hasChanged,
|
||||
() => {
|
||||
currentUser = userStore.getCurrentUser();
|
||||
const timestamp = new Date().getTime();
|
||||
currentUser.storedDataUrls.profilePictureUrl = `${currentUser.storedDataUrls.profilePictureUrl}?t=${timestamp}`;
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-container {
|
||||
@@ -192,5 +193,4 @@ onBeforeUnmount(() => {
|
||||
.nav-button:hover {
|
||||
@apply bg-[#903175] text-gray-200;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
<RouterLink to="/browse">
|
||||
<v-btn variant="tonal" density="default" class="mb-2 w-full">
|
||||
Découvre les autres créateurs
|
||||
|
||||
</v-btn>
|
||||
</RouterLink>
|
||||
|
||||
|
||||
@@ -142,9 +142,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<v-dialog v-model="showModal" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title class="text-center" style="margin-top: 30px; margin-bottom: 40px;">
|
||||
@@ -159,21 +156,15 @@
|
||||
<v-card-actions class="justify-end" style="margin-right: 20px;">
|
||||
<v-btn to="/" size="large" class="text-center" color="primary" text @click="showModal = false">Fermer</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
import { ref } from 'vue';
|
||||
|
||||
const client = useClient();
|
||||
|
||||
const showModal = ref(false);
|
||||
const name = ref("");
|
||||
const emailAddress = ref("");
|
||||
|
||||
67
src/views/main/LoginForm.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<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">
|
||||
<template #default>
|
||||
<v-btn density="comfortable" class="mb-2 w-full">
|
||||
<v-icon left>mdi-google</v-icon>
|
||||
Google
|
||||
</v-btn>
|
||||
</template>
|
||||
</google-login>
|
||||
<div class="w-full h-0.5 mt-4 mb-4" :style="{ backgroundColor: '#A30E79' }"></div>
|
||||
<v-btn density="comfortable" class="mb-2 w-full" @click="showEmailForm = !showEmailForm">
|
||||
<v-icon left>mdi-account</v-icon>
|
||||
Utilisateur
|
||||
</v-btn>
|
||||
<div v-if="showEmailForm" class="w-full mt-2">
|
||||
<v-text-field v-model="email" label="Courriel" variant="outlined" dense prepend-inner-icon="mdi-email" color="transparent" class="text-black"></v-text-field>
|
||||
<v-text-field v-model="password" label="Mot de passe" :type="showPassword ? 'text' : 'password'" variant="outlined" dense prepend-inner-icon="mdi-lock" append-inner-icon="mdi-eye" @click:append-inner="showPassword = !showPassword" color="transparent" class="text-black"></v-text-field>
|
||||
<v-btn class="w-full text-center text-white" :style="{ backgroundColor: '#A30E79' }" @click="login">Connecter</v-btn>
|
||||
<p class="mt-4 text-sm text-center">Si vous n'avez pas de compte, <a href="/register" class="text-blue-500">cliquez ici</a> pour en créer un.</p>
|
||||
<div v-if="errorSnackBar" class="mb-4 text-red-600">Nom d'utilisateur ou mot de passe invalide. <button class="text-red-600 ml-4" @click="errorSnackBar = false">Fermer</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {useAuthStore} from '@/stores/authStore.js';
|
||||
import {GoogleLogin} from "vue3-google-login";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
const errorSnackBar = ref(false);
|
||||
const showEmailForm = ref(false);
|
||||
const showPassword = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onFailure: {
|
||||
type: Function,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
async function login() {
|
||||
const result = await authStore.login(email.value, password.value);
|
||||
if (result === true) {
|
||||
props.onSuccess();
|
||||
} else {
|
||||
if (props.onFailure) {
|
||||
props.onFailure();
|
||||
}
|
||||
errorSnackBar.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,88 +0,0 @@
|
||||
<template>
|
||||
<div class="px-2">
|
||||
<div class="flex flex-col max-w-2xl mx-auto rounded-3xl shadow-2xl mt-2 mb-16">
|
||||
|
||||
<h1 class="text-center text-4xl bg-fuchsia-900 p-3 w-full rounded-t-3xl border-b-4 border-b-gray-200 font-sans text-white">
|
||||
Personnaliser votre profil
|
||||
</h1>
|
||||
|
||||
<ProfileBanner @updateProfilePicture="updateProfilePicture" @updateBannerPicture="updateBannerPicture" :user="currentUser"></ProfileBanner>
|
||||
<AboutYou :user="currentUser"></AboutYou>
|
||||
<SocialLinks :user="currentUser" @updateWebsiteIcon="updateWebsiteIcon"></SocialLinks>
|
||||
|
||||
<div class="sticky inset-x-0 bottom-0 flex justify-center px-4 pb-4">
|
||||
<div class="flex space-x-2">
|
||||
<v-btn class="save-btn" @click="saveForm">Enregistrer</v-btn>
|
||||
<router-link :to="`/${currentUser.userName}`">
|
||||
<v-btn class="save-btn">Retour</v-btn>
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-snackbar v-model="snackbar" :timeout="1000">
|
||||
Sauvegarde
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn color="green" variant="text" @click="snackbar = false">
|
||||
Fermer
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script async setup>
|
||||
import AboutYou from "@/views/main/Aboutyou.vue";
|
||||
import SocialLinks from "@/views/main/SocialLinks.vue";
|
||||
import ProfileBanner from "@/views/main/ProfileBanner.vue";
|
||||
import {onBeforeMount, ref} from "vue";
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import MyUserModel from "@/models/myUserModel.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const client = useClient();
|
||||
|
||||
const currentUser = ref(MyUserModel);
|
||||
const snackbar = ref(false);
|
||||
|
||||
onBeforeMount( () => {
|
||||
try {
|
||||
currentUser.value = userStore.getCurrentUser();
|
||||
} catch(error) {
|
||||
console.log("User not logged")
|
||||
}
|
||||
})
|
||||
|
||||
const profilePicture = ref(null);
|
||||
const bannerPicture = ref(null);
|
||||
const websiteIcon = ref(null);
|
||||
|
||||
const updateProfilePicture = (file) => {
|
||||
profilePicture.value = file;
|
||||
};
|
||||
|
||||
const updateBannerPicture = (file) => {
|
||||
bannerPicture.value = file;
|
||||
};
|
||||
|
||||
const updateWebsiteIcon = (file) => {
|
||||
websiteIcon.value = file;
|
||||
};
|
||||
|
||||
async function saveForm(){
|
||||
snackbar.value = true;
|
||||
await userStore.updateCurrentUser(client, currentUser.value, profilePicture.value, bannerPicture.value, websiteIcon.value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.save-btn {
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="px-5 py-2 bg-fuchsia-800 text-white" :style="{ backgroundColor: user.profileColors.menu }" >
|
||||
<div class="px-5 py-2 bg-fuchsia-800 text-white" :style="{ backgroundColor: user.colors.menu }">
|
||||
<div class="flex justify-center text-2xl">Photo de couverture</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :style="{ backgroundColor: user.profileColors.bannerTop || '#6B0065' }" class="flex h-4"></div>
|
||||
<div :style="{ backgroundColor: user.colors.bannerTop || '#6B0065' }" class="flex h-4"></div>
|
||||
<img :src="bannerImageUrl" alt="Banner Image" @error="handleBannerImageError" class="w-full object-cover">
|
||||
<div :style="{ backgroundColor: user.profileColors.bannerBottom || '#A30E79' }" class="h-4">
|
||||
<div :style="{ backgroundColor: user.colors.bannerBottom || '#A30E79' }" class="h-4">
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<div class="space-x-4 flex justify-center py-2">
|
||||
<v-btn @click="openColorPicker('top')"
|
||||
:style="{ backgroundColor: user.profileColors.bannerTop || '#6B0065', color: getTextColor(user.profileColors.bannerTop || '#6B0065') }">
|
||||
:style="{ backgroundColor: user.colors.bannerTop || '#6B0065', color: getTextColor(user.colors.bannerTop || '#6B0065') }">
|
||||
Haut
|
||||
</v-btn>
|
||||
<v-btn @click="openColorPicker('bottom')"
|
||||
:style="{ backgroundColor: user.profileColors.bannerBottom || '#A30E79', color: getTextColor(user.profileColors.bannerBottom || '#A30E79') }">
|
||||
:style="{ backgroundColor: user.colors.bannerBottom || '#A30E79', color: getTextColor(user.colors.bannerBottom || '#A30E79') }">
|
||||
Bas
|
||||
</v-btn>
|
||||
<v-btn @click="openColorPicker('accent')"
|
||||
:style="{ backgroundColor: user.profileColors.accent || '#23393B', color: getTextColor(user.profileColors.accent || '#23393B') }">
|
||||
:style="{ backgroundColor: user.colors.accent || '#23393B', color: getTextColor(user.colors.accent || '#23393B') }">
|
||||
Accent1
|
||||
</v-btn>
|
||||
<v-btn @click="openColorPicker('menu')"
|
||||
:style="{ backgroundColor: user.profileColors.menu || '#800080', color: getTextColor(user.profileColors.menu || '#800080') }">
|
||||
:style="{ backgroundColor: user.colors.menu || '#800080', color: getTextColor(user.colors.menu || '#800080') }">
|
||||
Menu
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -50,8 +50,8 @@
|
||||
<div class="flex justify-center text-2xl ">Photo de profil</div>
|
||||
</div>
|
||||
<div class="flex justify-center py-4">
|
||||
<img :src="profilePictureUrl"
|
||||
:style="{ borderColor: user.profileColors.accent || '#23393B' , borderWidth: '3px', borderStyle: 'solid' }"
|
||||
<img :src="logoImageUrl"
|
||||
:style="{ borderColor: user.colors.accent || '#23393B' , borderWidth: '3px', borderStyle: 'solid' }"
|
||||
class="rounded-full max-w-48 max-w-48"
|
||||
@error="handleProfileImageError"
|
||||
alt="Profile Image">
|
||||
@@ -85,53 +85,52 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, defineProps, onMounted} from 'vue';
|
||||
import MyUserModel from "@/models/myUserModel.js";
|
||||
import {ref} from 'vue';
|
||||
|
||||
const emit = defineEmits(["updateProfilePicture", "updateBannerPicture"]);
|
||||
const fallbackProfilePictureUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png';
|
||||
const fallbackBannerPictureUrl = '/images/usersmedia/HutopyProfile/banners/banner01.png';
|
||||
const emit = defineEmits(["updateProfilePicture", "updateBannerPicture"]);
|
||||
const fallbackProfilePictureUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png';
|
||||
const fallbackBannerPictureUrl = '/images/usersmedia/HutopyProfile/banners/banner01.png';
|
||||
|
||||
const props = defineProps({
|
||||
user: { type: MyUserModel },
|
||||
});
|
||||
const props = defineProps({
|
||||
user: {}
|
||||
});
|
||||
|
||||
const bannerImageUrl = ref(props.user.storedDataUrls.bannerPictureUrl);
|
||||
const profilePictureUrl = ref(props.user.storedDataUrls.profilePictureUrl);
|
||||
const bannerImageUrl = ref(props.user.images.banner);
|
||||
const logoImageUrl = ref(props.user.images.logo);
|
||||
|
||||
const bannerImage = ref(null);
|
||||
const profilePicture = ref(null);
|
||||
const bannerImage = ref(null);
|
||||
const profilePicture = ref(null);
|
||||
|
||||
const showColorPicker = ref(false);
|
||||
const selectedColor = ref('#F4F4F4');
|
||||
const colorTarget = ref('');
|
||||
const showColorPicker = ref(false);
|
||||
const selectedColor = ref('#F4F4F4');
|
||||
const colorTarget = ref('');
|
||||
|
||||
const handleProfileImageError = (event) => {
|
||||
event.target.src = fallbackProfilePictureUrl;
|
||||
}
|
||||
const handleProfileImageError = (event) => {
|
||||
event.target.src = fallbackProfilePictureUrl;
|
||||
}
|
||||
|
||||
const handleBannerImageError = (event) => {
|
||||
event.target.src = fallbackBannerPictureUrl;
|
||||
}
|
||||
const handleBannerImageError = (event) => {
|
||||
event.target.src = fallbackBannerPictureUrl;
|
||||
}
|
||||
|
||||
const onBannerFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const onBannerFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
bannerImageUrl.value = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
emit('updateBannerPicture', file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onProfileFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
profilePictureUrl.value = e.target.result;
|
||||
const onProfileFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
logoImageUrl.value = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
emit('updateProfilePicture', file);
|
||||
@@ -139,31 +138,31 @@ import {ref, defineProps, onMounted} from 'vue';
|
||||
|
||||
};
|
||||
|
||||
const openColorPicker = (target) => {
|
||||
const openColorPicker = (target) => {
|
||||
colorTarget.value = target;
|
||||
showColorPicker.value = true;
|
||||
};
|
||||
|
||||
const applyColor = () => {
|
||||
const applyColor = () => {
|
||||
if (colorTarget.value === 'top') {
|
||||
props.user.profileColors.bannerTop = selectedColor.value;
|
||||
props.user.colors.bannerTop = selectedColor.value;
|
||||
} else if (colorTarget.value === 'bottom') {
|
||||
props.user.profileColors.bannerBottom = selectedColor.value;
|
||||
props.user.colors.bannerBottom = selectedColor.value;
|
||||
} else if (colorTarget.value === 'accent') {
|
||||
props.user.profileColors.accent = selectedColor.value;
|
||||
props.user.colors.accent = selectedColor.value;
|
||||
} else if (colorTarget.value === 'menu') {
|
||||
props.user.profileColors.menu = selectedColor.value;
|
||||
props.user.colors.menu = selectedColor.value;
|
||||
}
|
||||
showColorPicker.value = false;
|
||||
};
|
||||
showColorPicker.value = false;
|
||||
};
|
||||
|
||||
/// Simple function to determine if the text should be white or black based on the background color
|
||||
const getTextColor = (bgColor) => {
|
||||
const color = bgColor.replace('#', '');
|
||||
const r = parseInt(color.substr(0, 2), 16);
|
||||
const g = parseInt(color.substr(2, 2), 16);
|
||||
const b = parseInt(color.substr(4, 2), 16);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return brightness > 155 ? '#000' : '#fff';
|
||||
};
|
||||
/// Simple function to determine if the text should be white or black based on the background color
|
||||
const getTextColor = (bgColor) => {
|
||||
const color = bgColor.replace('#', '');
|
||||
const r = parseInt(color.substr(0, 2), 16);
|
||||
const g = parseInt(color.substr(2, 2), 16);
|
||||
const b = parseInt(color.substr(4, 2), 16);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return brightness > 155 ? '#000' : '#fff';
|
||||
};
|
||||
</script>
|
||||
|
||||
23
src/views/main/Register.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="hidden sm:block" style="height: 40px"></div>
|
||||
<div>
|
||||
<div class="flex flex-col lg:flex-row items-center justify-center">
|
||||
<div class="max-w-[700px] min-w-[300px] mt-14">
|
||||
<img class="rounded-none sm:rounded-2xl sm:w-full mr-8" src="/images/hutopymedia/loginpage/loginhutopy.png" alt="hutopy login">
|
||||
</div>
|
||||
<div class="flex flex-col items-center min-w-[300px] m-12">
|
||||
<register-form></register-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<selected-footer></selected-footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SelectedFooter from "@/views/main/SelectedFooter.vue";
|
||||
import RegisterForm from "@/views/main/RegisterForm.vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
</script>
|
||||
195
src/views/main/RegisterForm.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col items-center min-w-[300px] m-4">
|
||||
<h1 class="text-center text-2xl font-bold mb-5">Inscription</h1>
|
||||
<div class="w-full h-0.5 mt-4 mb-4" :style="{ backgroundColor: '#A30E79' }"></div>
|
||||
<div class="w-full">
|
||||
<v-text-field
|
||||
v-model="userName"
|
||||
label="Username"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-account"
|
||||
color="transparent"
|
||||
class="text-black mb-4"
|
||||
></v-text-field>
|
||||
<div class="flex flex-wrap justify-between gap-4 mb-4">
|
||||
<v-text-field
|
||||
v-model="firstName"
|
||||
label="Prénom"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-account"
|
||||
color="transparent"
|
||||
class="text-black flex-grow min-w-[250px]"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="lastName"
|
||||
label="Nom"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-account"
|
||||
color="transparent"
|
||||
class="text-black flex-grow min-w-[250px]"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="emailAddress"
|
||||
label="Courriel"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-email"
|
||||
color="transparent"
|
||||
class="text-black mb-4"
|
||||
:error="emailError"
|
||||
:error-messages="emailErrorMessage"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Mot de passe"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-lock"
|
||||
append-inner-icon="mdi-eye"
|
||||
@click:append-inner="togglePasswordVisibility"
|
||||
color="transparent"
|
||||
class="text-black mb-4"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
label="Confirmer mot de passe"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
variant="outlined"
|
||||
dense
|
||||
prepend-inner-icon="mdi-lock"
|
||||
append-inner-icon="mdi-eye"
|
||||
@click:append-inner="togglePasswordVisibility"
|
||||
color="transparent"
|
||||
class="text-black mb-4"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="w-full text-center text-white"
|
||||
:style="{ backgroundColor: '#A30E79' }"
|
||||
@click="validateForm"
|
||||
>
|
||||
Créer
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for errors -->
|
||||
<v-dialog v-model="errorDialog" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title class="headline">Erreur</v-card-title>
|
||||
<v-card-text>{{ errorMessage }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="closeErrorDialog">Fermer</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useClient } from "@/plugins/api.js";
|
||||
|
||||
const router = useRouter();
|
||||
const client = useClient();
|
||||
|
||||
const firstName = ref('');
|
||||
const lastName = ref('');
|
||||
const emailAddress = ref('');
|
||||
const password = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const userName = ref('');
|
||||
const showPassword = ref(false);
|
||||
const errorDialog = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const emailError = ref(false);
|
||||
const emailErrorMessage = ref('');
|
||||
|
||||
// Messages d'erreur
|
||||
const ERROR_MESSAGES = {
|
||||
INVALID_EMAIL: 'E-mail doit être valide',
|
||||
PASSWORD_MISMATCH: 'Les mots de passe ne correspondent pas.',
|
||||
WEAK_PASSWORD: 'Le mot de passe doit contenir au moins 6 caractères, un chiffre, une lettre minuscule, une lettre majuscule et un caractère non-alphanumérique.',
|
||||
GENERIC_ERROR: 'Une erreur est survenue. Veuillez réessayer.',
|
||||
};
|
||||
|
||||
function togglePasswordVisibility() {
|
||||
showPassword.value = !showPassword.value;
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
const digitRegex = /\d/;
|
||||
const lowercaseRegex = /[a-z]/;
|
||||
const uppercaseRegex = /[A-Z]/;
|
||||
const nonAlphanumericRegex = /\W|_/;
|
||||
const lengthRequirement = password.length >= 6;
|
||||
|
||||
return digitRegex.test(password) &&
|
||||
lowercaseRegex.test(password) &&
|
||||
uppercaseRegex.test(password) &&
|
||||
nonAlphanumericRegex.test(password) &&
|
||||
lengthRequirement;
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /.+@.+\..+/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
emailError.value = false;
|
||||
emailErrorMessage.value = '';
|
||||
|
||||
if (!validateEmail(emailAddress.value)) {
|
||||
emailError.value = true;
|
||||
emailErrorMessage.value = ERROR_MESSAGES.INVALID_EMAIL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.value !== confirmPassword.value) {
|
||||
showErrorDialog(ERROR_MESSAGES.PASSWORD_MISMATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validatePassword(password.value)) {
|
||||
showErrorDialog(ERROR_MESSAGES.WEAK_PASSWORD);
|
||||
return;
|
||||
}
|
||||
|
||||
createUser();
|
||||
}
|
||||
|
||||
function showErrorDialog(message) {
|
||||
errorMessage.value = message;
|
||||
errorDialog.value = true;
|
||||
}
|
||||
|
||||
function closeErrorDialog() {
|
||||
errorDialog.value = false;
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
try {
|
||||
const userInformation = {
|
||||
FirstName: firstName.value,
|
||||
LastName: lastName.value,
|
||||
EmailAddress: emailAddress.value,
|
||||
UserName: userName.value,
|
||||
Password: password.value,
|
||||
};
|
||||
|
||||
await client.post('/api/Users', userInformation);
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
showErrorDialog(ERROR_MESSAGES.GENERIC_ERROR);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -7,15 +7,15 @@
|
||||
|
||||
<div class="flex flex-row justify-center space-x-10 py-10">
|
||||
<a href="https://www.facebook.com/profile.php?id=61556819217561">
|
||||
<img class="max-h-10" src="/images/hutopymedia/icons/pink/facebookpink.png" alt="Facebook">
|
||||
<v-icon size="40px" class="text-fuchsia-900">mdi-facebook</v-icon>
|
||||
</a>
|
||||
|
||||
<a href="https://www.instagram.com/hutopy.inc/">
|
||||
<img class="max-h-10" src="/images/hutopymedia/icons/pink/instagrampink.png" alt="Instagram">
|
||||
<v-icon size="40px" class="text-fuchsia-900">mdi-instagram</v-icon>
|
||||
</a>
|
||||
|
||||
<a href="https://x.com/Hutopyinc/">
|
||||
<img class="max-h-10" src="/images/hutopymedia/icons/pink/xpink.png" alt="X">
|
||||
<img src="/images/hutopymedia/icons/x.svg" width="34px" height="34px" class="mt-1 filter-fushia ">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<v-btn variant="plain"> Guide pour les créateurs</v-btn>
|
||||
</router-link>
|
||||
<router-link to="/termsandconditions">
|
||||
<v-btn variant="plain"> TermsAndConditions </v-btn>
|
||||
<v-btn variant="plain">Termes et Conditions </v-btn>
|
||||
</router-link>
|
||||
<router-link to="/contentpolicy">
|
||||
<v-btn variant="plain"> Politique de Contenu </v-btn>
|
||||
@@ -42,9 +42,14 @@
|
||||
<v-btn variant="plain"> Frais</v-btn>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter-fushia{
|
||||
filter: invert(14%) sepia(60%) saturate(4103%) hue-rotate(285deg) brightness(84%) contrast(93%);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,66 +1,37 @@
|
||||
<template>
|
||||
<script setup>
|
||||
import SiteMenu from "@/views/main/SiteMenu.vue";
|
||||
import SubscriptionList from "@/views/creators/SubscriptionList.vue";
|
||||
import {useAuthStore} from "@/stores/authStore.js";
|
||||
|
||||
const authStore = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="relative h-full overflow-y-auto custom-scrollbar">
|
||||
|
||||
<nav class="h-full grid grid-rows-[60px_1fr_auto]">
|
||||
<div></div>
|
||||
<div class="pt-4 px-4">
|
||||
<h2>Subscriptions</h2>
|
||||
|
||||
<ul class="space-y">
|
||||
<li>
|
||||
<RouterLink to="/@leffet">
|
||||
<div class="nav-button">
|
||||
<v-icon class="mx-2">mdi-account</v-icon>
|
||||
L'Effet
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/@chloebeaugrand">
|
||||
<div class="nav-button">
|
||||
<v-icon class="mx-2">mdi-account</v-icon>
|
||||
Chloe Beaugrand
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/@guillaumeaime">
|
||||
<div class="nav-button">
|
||||
<v-icon class="mx-2">mdi-account</v-icon>
|
||||
Guillaume Aime
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/@mathieucaron">
|
||||
<div class="nav-button">
|
||||
<v-icon class="mx-2">mdi-account</v-icon>
|
||||
Mathieu Caron
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/@arps">
|
||||
<div class="nav-button">
|
||||
<v-icon class="mx-2">mdi-account</v-icon>
|
||||
ARPS
|
||||
</div>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="authStore.isAuthenticated" class="pt-4 px-4">
|
||||
<h2>Abonnements</h2>
|
||||
<subscription-list>
|
||||
<template v-slot:default>
|
||||
<span>Aucun abonnement</span>
|
||||
</template>
|
||||
</subscription-list>
|
||||
</div>
|
||||
<div v-else>
|
||||
</div>
|
||||
|
||||
<div class="border-t w-full py-10">
|
||||
<SiteMenu></SiteMenu>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-button {
|
||||
@apply rounded p-1 m-1 text-gray-800 font-sans;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
@apply bg-[#903175] text-gray-200;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply font-sans font-bold ml-2;
|
||||
@@ -74,8 +45,5 @@ aside {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #903175 #f1f1f1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SiteMenu from "@/views/main/SiteMenu.vue";
|
||||
</script>
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
|
||||
<div class="flex flex-row justify-center pb-4 pt-2 py-4">
|
||||
<!-- Facebook -->
|
||||
<a href="https://www.facebook.com/profile.php?id=61556819217561">
|
||||
<img src="/images/hutopymedia/icons/pink/facebookpink.png" alt="Facebook Logo">
|
||||
<a href="https://www.facebook.com/profile.php?id=61556819217561" class="social">
|
||||
<v-icon>mdi-facebook</v-icon>
|
||||
</a>
|
||||
<!-- Instagram -->
|
||||
<a href="https://www.instagram.com/hutopy.inc/">
|
||||
<img src="/images/hutopymedia/icons/pink/instagrampink.png" alt="Instagram Logo">
|
||||
<a href="https://www.instagram.com/hutopy.inc/" class="social">
|
||||
<v-icon>mdi-instagram</v-icon>
|
||||
</a>
|
||||
<!-- X / Twitter -->
|
||||
<a href="https://twitter.com/Hutopyinc">
|
||||
<img src="/images/hutopymedia/icons/pink/xpink.png" alt="X Logo">
|
||||
<a href="https://twitter.com/Hutopyinc" class="social">
|
||||
<img src="/images/hutopymedia/icons/x.svg" width="23px" height="23px" class="mb-5 mr-2">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -41,11 +41,8 @@
|
||||
@apply text-purple-800 bg-gray-50;
|
||||
}
|
||||
|
||||
img {
|
||||
.social {
|
||||
@apply m-2 w-10 h-10;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<div ref="container">
|
||||
<div class="text-center text-2xl py-4 border-t-4">Liens des réseaux sociaux et de votre site</div>
|
||||
<div class="px-5 py-2 flex flex-col space-y-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Instagram</label>
|
||||
</div>
|
||||
<v-text-field v-model="props.user.socialNetworks.instagramUrl" label="Instagram" variant="outlined"></v-text-field>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">TikTok</label>
|
||||
</div>
|
||||
<v-text-field v-model="props.user.socialNetworks.tikTokUrl" label="TikTok" variant="outlined"></v-text-field>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Facebook</label>
|
||||
</div>
|
||||
<v-text-field v-model="props.user.socialNetworks.facebookUrl" label="Facebook" variant="outlined"></v-text-field>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">X</label>
|
||||
</div>
|
||||
<v-text-field v-model="props.user.socialNetworks.xUrl" label="X" variant="outlined"></v-text-field>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">LinkedIn</label>
|
||||
</div>
|
||||
<v-text-field v-model="props.user.socialNetworks.linkedInUrl" label="LinkedIn" variant="outlined"></v-text-field>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label class="text-lg">Site Web</label>
|
||||
</div>
|
||||
<v-text-field v-model="props.user.socialNetworks.yourWebsiteUrl" label="Site Web" variant="outlined"></v-text-field>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<label class="text-lg mr-4">Icône pour votre site web *svg</label>
|
||||
<v-file-input
|
||||
v-model="iconFile"
|
||||
accept=".png, .jpeg, .jpg"
|
||||
hint="png, jpeg or jpg"
|
||||
label="Téléverser une icône"
|
||||
@change="onFileChange">
|
||||
</v-file-input>
|
||||
</div>
|
||||
<div v-if="iconUrl" class="flex justify-center">
|
||||
<img :src="iconUrl" alt="Icon" class="icon-preview">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, ref } from 'vue';
|
||||
import MyUserModel from "@/models/myUserModel.js";
|
||||
|
||||
const props = defineProps({
|
||||
user: { type: MyUserModel },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["updateWebsiteIcon"]);
|
||||
|
||||
const iconUrl = ref(props.user.storedDataUrls.websiteIconUrl);
|
||||
const iconFile = ref(null);
|
||||
|
||||
const onFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
iconUrl.value = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
emit("updateWebsiteIcon", file)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-preview {
|
||||
max-width: 250px;
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -47,7 +47,7 @@
|
||||
<script async setup>
|
||||
import { onBeforeMount, ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {useUserStore} from "@/stores/user.js";
|
||||
import {useUserStore} from "@/stores/userStore.js";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
@@ -75,10 +75,8 @@ const transactionCount = computed(() => userTransactions.value.length);
|
||||
|
||||
onBeforeMount( () => {
|
||||
try {
|
||||
const myUser = userStore.getCurrentUser();
|
||||
|
||||
userTransactions.value = myUser.userTransactions;
|
||||
totalBalance.value = myUser.totalBalance;
|
||||
userTransactions.value = userStore.user.userTransactions;
|
||||
totalBalance.value = userStore.user.totalBalance;
|
||||
} catch (error) {
|
||||
navigateToHome();
|
||||
}
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
<template>
|
||||
|
||||
<!-- "Mobile -->
|
||||
<v-row class="fluid hidden-md-and-up social-mobile-container"
|
||||
style="margin-top: -10px; position: relative; z-index: 0; " hidden-md-and-down>
|
||||
<v-col cols="12" class="pa-0" style="width: 100vw; overflow: hidden;">
|
||||
<v-img class="profile-banner" max-height="375" :src="imageSrc" cover
|
||||
style="box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- "PurpleLine" -->
|
||||
<v-row style="background-color: #6b0065; height: 5px; box-shadow: rgba(0, 0, 0, 0.7);">
|
||||
</v-row>
|
||||
|
||||
<!-- "Mobile-Profile" -->
|
||||
<!-- "Profile picture" -->
|
||||
<v-row class="d-flex justify-center align-center d-sm-none" style="margin-top: 50px; margin-bottom: -10px">
|
||||
<v-img :src="profilePicture" class="elevation-4 mobile-profile-picture-creator"></v-img>
|
||||
</v-row>
|
||||
|
||||
<!-- User Social Network Links -->
|
||||
<v-container style="margin-top: -70px; padding: 0;" class="hidden-md-and-up social-container-mobile-icons"
|
||||
hidden-md-and-down>
|
||||
|
||||
<!-- Facebook -->
|
||||
<v-row no-gutters class="d-flex justify-space-between align-center" style="margin-left: 3%; margin-right: 3%;">
|
||||
<v-col cols="2" sm="2" xs="3" style="margin-top: 60px; z-index: 50;" class="d-flex justify-center align-center">
|
||||
<a href="https://www.facebook.com/chloegestionmedias">
|
||||
<v-img class="socialicons-mobile" src="/images/hutopymedia/icons/black/facebookblack.png"
|
||||
alt="Facebook"></v-img>
|
||||
</a>
|
||||
</v-col>
|
||||
|
||||
<!-- Profile picture - Small -->
|
||||
<v-col cols="4" sm="4" xs="0" class="hidden-xs">
|
||||
<v-row class="d-flex justify-center align-center">
|
||||
<v-img :src="profilePicture" class="elevation-4 mobile-profile-picture-creator"></v-img>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<!-- Instagram -->
|
||||
<v-col cols="2" sm="2" xs="3" style="margin-top: 60px; z-index: 50;" class="d-flex justify-center align-center">
|
||||
<a href="https://www.instagram.com/chloe.photo_gms">
|
||||
<v-img class="socialicons-mobile" src="/images/hutopymedia/icons/black/instagramblack.png"
|
||||
alt="Instagram"></v-img>
|
||||
</a>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- User informations Name title and description -->
|
||||
<v-row class="social-container-mobile">
|
||||
<v-col cols="12" xs="12" sm="4" class="d-flex justify-center">
|
||||
<v-row class="d-flex justify-center">
|
||||
{{ name }}
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-spacer xs="12" sm="4"></v-spacer>
|
||||
<v-col cols="12" xs="12" sm="4" class="d-flex justify-center">
|
||||
<v-row class="d-flex justify-center">
|
||||
{{ title }}
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-container style="border-bottom-left-radius: 15px; margin-top: -12px; border-bottom-right-radius: 15px;"
|
||||
class="social-icon-group-mobile">
|
||||
<v-expansion-panels style="min-width: 320px;">
|
||||
<v-col cols="12" offset="-1">
|
||||
<v-expansion-panel class="background-pink text-justify" style="color: white;" text="
|
||||
|
||||
Axé sur l’être humain et le désir de performer, j’ai trouvé mon X avec les médias sociaux → Mon but est d’aider les entreprises à communiquer et à prospérer en renforçant leur image de marque à travers leur communauté!
|
||||
Chaque collaboration est une aventure marketing inspirante!
|
||||
|
||||
|
||||
Je suis authentique, créative, stratégique, caféinée et à l’écoute. Et surtout, j’ai hâte de travailler avec vous!
|
||||
" title="À propos de moi.">
|
||||
</v-expansion-panel>
|
||||
</v-col>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- Bannière Pc -->
|
||||
<v-row class="fluid hidden-sm-and-down" style="margin-top: -65px; position: relative; z-index: 0; "
|
||||
hidden-sm-and-up>
|
||||
<v-col cols="12" class="pa-0" style="width: 100vw; overflow: hidden;">
|
||||
<v-img class="profile-banner" max-height="375" :src="imageSrc" cover
|
||||
style="box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="hidden-sm-and-down" style="background-color: #6b0065; height: 5px; box-shadow: rgba(0, 0, 0, 0.7);"
|
||||
hidden-sm-and-up>
|
||||
</v-row>
|
||||
|
||||
<!-- "Core (Menu / Center / Donation)" -->
|
||||
<v-row>
|
||||
|
||||
<!-- Profile Info Picture name title & description -->
|
||||
<v-col class="middle-col" style="z-index: 5;" cols="12" xs="12" sm="12" md="6" lg="5" xl="4" xxl="4">
|
||||
<v-container class="profile-container hidden-sm-and-down" hidden-md-and-up>
|
||||
<v-container>
|
||||
<v-img :src="profilePicture" class="elevation-4 mobile-profile-picture-creator"></v-img>
|
||||
<!-- User informations Name & title -->
|
||||
<v-container class="background-profile-container" style="margin-top: -8%;">
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="7" offset="2" class="social-container">
|
||||
<h1 class="name-info">{{ name }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="6" offset="2" class="social-container2">
|
||||
<h1 class="occupation-info">{{ title }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- User Social Network Links -->
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="7" style="margin-top: 1%; margin-bottom: -2%;">
|
||||
<v-row>
|
||||
<a href="https://www.facebook.com/chloegestionmedias">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/facebookblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
<a href="https://www.instagram.com/chloe.photo_gms">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/instagramblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col background-color="primary"></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Description -->
|
||||
<v-row>
|
||||
<v-container
|
||||
style="border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; margin-top: 15px;">
|
||||
<v-expansion-panels style="min-width: 320px;">
|
||||
<v-col cols="12" offset="-1">
|
||||
<v-expansion-panel class="background-pink text-justify" style="color: white;" text="
|
||||
|
||||
Axé sur l’être humain et le désir de performer, j’ai trouvé mon X avec les médias sociaux → Mon but est d’aider les entreprises à communiquer et à prospérer en renforçant leur image de marque à travers leur communauté!
|
||||
Chaque collaboration est une aventure marketing inspirante!
|
||||
|
||||
|
||||
Je suis authentique, créative, stratégique, caféinée et à l’écoute. Et surtout, j’ai hâte de travailler avec vous!
|
||||
" title="À propos de moi.">
|
||||
</v-expansion-panel>
|
||||
</v-col>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</v-container>
|
||||
|
||||
<!-- Card nouvelle boutique -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container style="margin-top: -15px;">
|
||||
<v-img src="images/usersmedia/chloebeaugrand/pictures/posts/postsChloeBeaugrand01.png"
|
||||
title="Savoir faire"></v-img>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px; margin-bottom: -20px" class="card-date">
|
||||
23-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">Quand on observe des gens
|
||||
réaliser un
|
||||
Rubik’s Cube ça a l’air si facile pour eux qu’on est vraiment impressionnés et peut-être même
|
||||
découragés…<br><br>
|
||||
|
||||
Mais saviez-vous que pour résoudre ce casse-tête, il suffit d’une série de mouvements exécutés à
|
||||
répétitions
|
||||
dans le même ordre jusqu’à la fin?<br><br>
|
||||
|
||||
La gestion de médias sociaux peut donner l’impression d’être un vrai casse-tête!
|
||||
|
||||
La clé c’est de comprendre chaque élément individuellement et ensuite en faire un tout.
|
||||
</p>
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px" class="Comments-font"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
|
||||
<!-- Card nouvelle boutique -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container style="margin-top: -15px;">
|
||||
<v-img src="images/usersmedia/chloebeaugrand/pictures/posts/postsChloeBeaugrand02.png"
|
||||
title="Guillaumem"></v-img>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px; margin-bottom: -20px" class="card-date">
|
||||
23-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">
|
||||
Que ce soit pour un portrait créatif, la gestion totales de tes médias sociaux où pour devenir
|
||||
autonome à
|
||||
les gérer par toi-même → Je suis là pour ça!
|
||||
<br><br>
|
||||
|
||||
Je te promets qu’on passera un bon moment ET que tu auras des résultats à la hauteur de tes attentes
|
||||
<v-icon icon=" mdi-emoticon"></v-icon>
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
let imageSrc = '/images/usersmedia/chloebeaugrand/banners/bannerChloeBeaugrand01.png';
|
||||
let profilePicture = '/images/usersmedia/chloebeaugrand/profilepictures/profileChloeBeaugrand01.png';
|
||||
let name = 'Chloé Beaugrand'
|
||||
let title = 'Spécialiste en média'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.Comments-font {
|
||||
font-size: .1rem;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
margin-top: -10px;
|
||||
margin-left: 15%;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
margin-left: 15%;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -16%;
|
||||
}
|
||||
|
||||
.background-profile-container {
|
||||
background-color: #ececec;
|
||||
color: white;
|
||||
border-top: 3px solid #a30e79;
|
||||
border-right: 3px solid #a30e79;
|
||||
font-weight: 700;
|
||||
font-size: 1.15rem;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3)
|
||||
}
|
||||
|
||||
.background-pink {
|
||||
background-color: #231f20;
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 3px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.socialicons-mobile {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 27px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.youtube-card {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
border-radius: 15px;
|
||||
background-color: #f4f4f4;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4)
|
||||
}
|
||||
|
||||
.card-date {
|
||||
margin-left: 10px;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -20px;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.social-container {
|
||||
background-color: #8c5536;
|
||||
border-top-right-radius: 30px;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
background-color: #231f20;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container-mobile {
|
||||
background-color: #8c5536;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-icon-group-mobile {
|
||||
background-color: #272526;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
border-radius: 100px;
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
border-radius: 50%;
|
||||
max-width: 150px;
|
||||
border: 4px solid white;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
margin-top: 25px;
|
||||
min-height: 200px
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,366 +0,0 @@
|
||||
<template>
|
||||
<!-- Bannière Pc -->
|
||||
<v-row class="fluid" style="margin-top: -65px; position: relative; z-index: 0;">
|
||||
<v-col cols="12" class="pa-0" style="width: 100vw; overflow: hidden;">
|
||||
<v-img class="profile-banner" max-height="375" :src="imageSrc" cover
|
||||
style="box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- "Core (Menu / Center / Donation)" -->
|
||||
<v-row>
|
||||
|
||||
<!-- Profile Info Picture name title & description -->
|
||||
<v-col class="middle-col" style="z-index: 5;" cols="12" xs="12" sm="12" md="6" lg="5" xl="4" xxl="4">
|
||||
<v-container class="profile-container">
|
||||
<v-container>
|
||||
<v-img :src="profilePicture" class="elevation-4 mobile-profile-picture-creator"></v-img>
|
||||
<!-- User informations Name & title -->
|
||||
<v-container class="background-profile-container" style="margin-top: -8%;">
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="7" offset="2" class="social-container">
|
||||
<h1 class="name-info">{{ name }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="6" offset="2" class="social-container2">
|
||||
<h1 class="occupation-info">{{ title }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- User Social Network Links -->
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="7" style="margin-top: 1%; margin-bottom: -2%;">
|
||||
<v-row>
|
||||
<a href="https://www.facebook.com/GuillaumeMousseau222">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/facebookblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
<a href="https://www.instagram.com/p/C2f-3UnNdfX/">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/instagramblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
|
||||
<a href="https://www.tiktok.com/@guillaumeaime">
|
||||
<img class="socialicons invert-color" src="/images/hutopymedia/icons/white/tiktokwhite.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col background-color="primary"></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Description -->
|
||||
<v-row>
|
||||
<v-container
|
||||
style="border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; margin-top: 15px;">
|
||||
<v-expansion-panels style="min-width: 320px;">
|
||||
<v-col cols="12" offset="-1">
|
||||
<v-expansion-panel class="background-pink text-justify" style="color: white;"
|
||||
text="Mettre en lumière le côté humain des entrepreneurs. Chaque service, chaque produit est porteur d’une histoire, d’une passion, d’une vision unique. Mon objectif est de faire rayonner cette unicité, de créer des connexions authentiques entre ces entrepreneurs et leurs clients potentiels. Parce que derrière chaque entreprise, il y a des personnes inspirantes qui méritent d’être entendues et comprises. Et toi, quel est ton objectif pour cette année?"
|
||||
title="À propos : Ma mission est claire :">
|
||||
</v-expansion-panel>
|
||||
</v-col>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</v-container>
|
||||
|
||||
<!-- Card youtube balado -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
MON PREMIER BALADO</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video/image & text -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%">
|
||||
<v-container>
|
||||
<iframe class="card-youtube" style="margin-left: 2.1%; width: 96%;"
|
||||
src="https://www.youtube.com/embed/pf95whtA_xs?start=0" title="Guillaumem" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen></iframe>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px" class="card-date">
|
||||
24-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -50px; font-size: 1em">C’est un honneur de vous
|
||||
présenter
|
||||
mon tout premier balado. Dans ce premier épisode, les passionnés de cinéma et de gadgets seront
|
||||
particulièrement gâtés, car je dévoile les gadgets que j'utilise professionnellement. Par la suite, je
|
||||
partage mon itinéraire professionnel peu conventionnel : de mes débuts dans le secteur bovin à travers
|
||||
le
|
||||
Canada, à mon poste actuel comme directeur marketing chez Journal Mobile, jusqu'à la direction de mon
|
||||
agence
|
||||
créative, Alliés. Enfin, je vous invite à découvrir un autre aspect de ma vie à travers ma page
|
||||
personnelle,
|
||||
"Guillaume aime", où je partage tout ce qui me passionne.</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
|
||||
<!-- Card nouvelle boutique -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
NOUVELLE BOUTIQUE À SAINT-HYACINTHE</h1>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video/image & text -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%">
|
||||
<v-container>
|
||||
<v-img src="images/usersmedia/guillaumeMousseau/pictures/posts/nouvelleboutique.jpg"
|
||||
title="Guillaumem"></v-img>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px" class="card-date">
|
||||
24-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">J’ai découvert une nouvelle
|
||||
boutique
|
||||
à St-Hyacinthe qui embrasse exactement ma philosophie derrière la page Guillaume Aime: faire découvrir
|
||||
le
|
||||
savoir-faire québécois !
|
||||
|
||||
J’y ai croisé Carl Vaillancourt, copropriétaire de l’entreprise Espace Karibou au Centre-ville
|
||||
Saint-Hyacinthe ainsi que mon ami Marc-Olivier Hébert de la Fondation L'effet.
|
||||
|
||||
Je vous invite à découvrir cette boutique sur la rue Cascade à St-Hyacinthe. Bravo à Karianne Hamel et
|
||||
Carl
|
||||
pour ce projet!</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- Card nouvelle boutique -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
C'EST PARTI POUR 2024!</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video/image & text -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%">
|
||||
<v-container>
|
||||
<v-img src="images/usersmedia/guillaumeMousseau/pictures/posts/cestparti.jpg"
|
||||
title="Guillaumem"></v-img>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px" class="card-date">
|
||||
22-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">C'est parti pour 2024 ! De
|
||||
retour au
|
||||
travail officiellement ce matin afin d'aider les entreprises de la région à rayonner ! Mettre en
|
||||
lumière les
|
||||
entrepreneurs et leur unicité, c'est ma passion ! Si jamais tu as besoin d'aide dans ce domaine,
|
||||
n'hésite
|
||||
pas à me contacter.</p>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
let imageSrc = '/images/usersmedia/guillaumeMousseau/banners/bannerGuillaumeMousseau01.png';
|
||||
let profilePicture = '/images/usersmedia/guillaumeMousseau/profilepictures/profileGuillaumeMousseau01.png';
|
||||
let name = 'Guillaume Mousseau'
|
||||
let title = 'Créateur de contenus'
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.invert-color {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.name-info {
|
||||
margin-top: -10px;
|
||||
margin-left: 15%;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
margin-left: 15%;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -16%;
|
||||
}
|
||||
|
||||
.background-profile-container {
|
||||
background-color: #ececec;
|
||||
color: white;
|
||||
border-top: 3px solid #a30e79;
|
||||
border-right: 3px solid #a30e79;
|
||||
font-weight: 700;
|
||||
font-size: 1.15rem;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3)
|
||||
}
|
||||
|
||||
.background-pink {
|
||||
background-color: #cc6f91;
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 3px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.socialicons-mobile {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 27px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.youtube-card {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
border-radius: 15px;
|
||||
background-color: #f4f4f4;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4)
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.card-date {
|
||||
margin-left: 10px;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -20px;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.social-container {
|
||||
background-color: #006d77;
|
||||
border-top-right-radius: 30px;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
background-color: #0baab2;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container-mobile {
|
||||
background-color: #006d77;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-icon-group-mobile {
|
||||
background-color: #0baab2;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
@apply rounded-full border-4 border-white;
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
margin-top: 25px;
|
||||
min-height: 200px
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,656 +0,0 @@
|
||||
<template>
|
||||
|
||||
<v-row class="fluid" style="margin-top: -65px; position: relative; z-index: 0; ">
|
||||
<v-col cols="12" class="pa-0" style="width: 100vw; overflow: hidden;">
|
||||
<v-img class="profile-banner" max-height="375" :src="imageSrc" cover
|
||||
style="box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="background-color: #6b0065; height: 5px; box-shadow: rgba(0, 0, 0, 0.7);">
|
||||
</v-row>
|
||||
|
||||
<!-- "Core (Menu / Center / Donation)" -->
|
||||
<v-row>
|
||||
|
||||
<!-- Profile Info Picture name title & description -->
|
||||
<v-col class="middle-col" style="z-index: 5;" cols="12" xs="12" sm="12" md="6" lg="5" xl="4" xxl="4">
|
||||
<v-container class="profile-container">
|
||||
<v-container>
|
||||
<v-img :src="profilePicture" class="elevation-4 mobile-profile-picture-creator"></v-img>
|
||||
<!-- User informations Name & title -->
|
||||
<v-container class="background-profile-container" style="margin-top: -8%;">
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="7" offset="2" class="social-container">
|
||||
<h1 class="name-info">{{ name }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="6" offset="2" class="social-container2">
|
||||
<h1 class="occupation-info">{{ title }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- User Social Network Links -->
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="7" style="margin-top: 1%; margin-bottom: -2%;">
|
||||
<v-row>
|
||||
<a href="https://www.facebook.com/Hutopy">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/facebookblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
<a href="https://fondationleffet.ca/">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/leffetblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col background-color="primary"></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Description -->
|
||||
<v-row>
|
||||
<v-container
|
||||
style="border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; margin-top: 15px;">
|
||||
<v-expansion-panels style="min-width: 320px;">
|
||||
<v-col cols="12" offset="-1">
|
||||
<v-expansion-panel class="background-pink text-justify" style="color: white;"
|
||||
text="La Fondation L'effet est un organisme de bienfaisance qui a trois fins principales, toutes pour prendre soin de son prochain. Notre principal objectif est de promouvoir l’éducation, en fournissant les ressources nécessaires et en donnant des conférences afin d’encourager la persévérance scolaire. Nous soulageons également les conditions des personnes atteintes d'un handicap en offrant des programmes récréatifs et d’autres services de soutien visant à appuyer leur mieux-être physique, mental et émotif afin qu’ils deviennent et demeurent des acteurs importants au sein de la collectivité. La Fondation L’effet encourage aussi les autres organismes et aide les initiatives individuelles ou corporatives à faire une différence positive dans leur communauté."
|
||||
title="Notre mission">
|
||||
</v-expansion-panel>
|
||||
</v-col>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</v-container>
|
||||
<v-container>
|
||||
</v-container>
|
||||
|
||||
<v-container style="margin-top: -30px">
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
DOCU-PODCAST</h1>
|
||||
</v-col>
|
||||
|
||||
<!--<v-col class="text-right">
|
||||
<v-btn class="btn-card-options" flat tile elevation="0">
|
||||
<v-icon style="color: #6e6e6e; font-size: 24px;">mdi-dots-vertical</v-icon>
|
||||
</v-btn> (a intégrer)
|
||||
</v-col> -->
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video/image & text -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container>
|
||||
<iframe class="card-youtube" style="margin-left: 2.1%; width: 96%;"
|
||||
src="https://www.youtube.com/embed/3MqKVf1CEIQ?si=aV03Xq2ZNwlv1DWu" title="Guillaumem"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen></iframe>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px">
|
||||
24-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">
|
||||
|
||||
C’est un honneur de vous
|
||||
En exclusivité : Voici l'Épisode 8 de la Saison 2 du Docu-Podcast de Cédieu Léveillé avec le Fondateur
|
||||
de la
|
||||
Fondation l'Effet, Marc-Olivier Hébert. Cette homme est un Ange sur 2 pattes et un Master de
|
||||
l'Introspection :
|
||||
'' Quand j'ai Compris que Mon Seul Obstacle Était Moi-même '' ! Merci de continuer à faire partie de
|
||||
cette
|
||||
AVENTURE et à être les PIONNIERS de mon Docu-Podcast. Seuls vous, Fidèles PIADINS avez le DROIT à
|
||||
l'ENTIÈRETÉ
|
||||
de ces images !!!
|
||||
Marc-Olivier
|
||||
Marc-Olivier Hébert
|
||||
Le nom de la Fondation vient de l'origine du phénomène que l’on nomme ‘’L’effet Papillon’’. Cette
|
||||
théorie
|
||||
implique que le battement d’ailes d’un papillon au Brésil peut provoquer une tempête au Texas.
|
||||
|
||||
Selon l’expression, inventée par le météorologue Edward Lorenz, il suffit de modifier de façon infime
|
||||
un
|
||||
paramètre dans un modèle météo pour que celui-ci s’amplifie progressivement et provoque, à long terme,
|
||||
des
|
||||
changements colossaux. Cette notion ne concerne plus seulement la météo, mais s’applique également aux
|
||||
sciences humaines et à l’environnement. <br><br>
|
||||
|
||||
L'effet papillon est matérialisé par une chaîne d'événements qui se suivent les uns les autres et dont
|
||||
le
|
||||
précédent influe sur le suivant. Ainsi, on part d'un événement minuscule au début de la chaîne pour
|
||||
arriver à
|
||||
une chose gigantesque.
|
||||
<br><br>
|
||||
|
||||
Chaque action que nous entreprenons crée automatiquement un résultat. Il est à nous de choisir qu’il
|
||||
soit
|
||||
toujours positif.
|
||||
<br><br>
|
||||
|
||||
Donc, voici toute l’essence de la nature de cette fondation. Que de petites actions et gestes positifs
|
||||
créeront automatiquement de grandes et merveilleuses différences positives dans la vie des
|
||||
participants
|
||||
et des
|
||||
bénéficiaires.
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- Le nom -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
C'EST PARTI POUR 2024!</h1>
|
||||
</v-col>
|
||||
<!--
|
||||
<v-col class="text-right">
|
||||
<v-btn class="btn-card-options" flat tile elevation="0">
|
||||
<v-icon style="color: #6e6e6e; font-size: 24px;">mdi-dots-vertical</v-icon>
|
||||
</v-btn> (a intégrer)
|
||||
</v-col> -->
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video/image & text -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container>
|
||||
<v-img src="images/usersmedia/leffet/profilepictures/leffetProfile01.png" title="Guillaumem"></v-img>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px">
|
||||
22-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">Le nom de la Fondation vient de
|
||||
l'origine du phénomène que l’on nomme ‘’L’effet Papillon’’. Cette théorie implique que le battement
|
||||
d’ailes d’un papillon au Brésil peut provoquer une tempête au Texas.<br><br>
|
||||
|
||||
Selon l’expression, inventée par le météorologue Edward Lorenz, il suffit de modifier de façon infime
|
||||
un paramètre dans un modèle météo pour que celui-ci s’amplifie progressivement et provoque, à long
|
||||
terme,
|
||||
des changements colossaux. Cette notion ne concerne plus seulement la météo, mais s’applique également
|
||||
aux
|
||||
sciences humaines et à l’environnement.<br>
|
||||
<br>
|
||||
|
||||
L'effet papillon est matérialisé par une chaîne d'événements qui se suivent les uns les autres et dont
|
||||
le précédent influe sur le suivant. Ainsi, on part d'un événement minuscule au début de la chaîne pour
|
||||
arriver à une chose gigantesque.<br>
|
||||
<br>
|
||||
|
||||
|
||||
Chaque action que nous entreprenons crée automatiquement un résultat. Il est à nous de choisir qu’il
|
||||
soit toujours positif.
|
||||
|
||||
Donc, voici toute l’essence de la nature de cette fondation. Que de petites actions et gestes positifs
|
||||
créeront automatiquement de grandes et merveilleuses différences positives dans la vie des
|
||||
participants et
|
||||
des bénéficiaires.
|
||||
</p>
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
let imageSrc = '/images/usersmedia/leffet/banners/banner02.png';
|
||||
let profilePicture = '/images/usersmedia/leffet/profilepictures/leffetProfile01.png';
|
||||
let name = 'L\'EFFET'
|
||||
let title = 'Page officiel'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.name-info {
|
||||
margin-top: -10px;
|
||||
margin-left: 15%;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
margin-left: 15%;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -16%;
|
||||
}
|
||||
|
||||
.background-profile-container {
|
||||
background-color: #ececec;
|
||||
color: white;
|
||||
border-top: 3px solid #a30e79;
|
||||
border-right: 3px solid #a30e79;
|
||||
font-weight: 700;
|
||||
font-size: 1.15rem;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3)
|
||||
}
|
||||
|
||||
.background-pink {
|
||||
background-color: #c9017d;
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 3px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.socialicons-mobile {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 27px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.youtube-card {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
border-radius: 15px;
|
||||
background-color: #f4f4f4;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4)
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.social-container {
|
||||
background-color: #79cc02;
|
||||
border-top-right-radius: 30px;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
background-color: #fbc702;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container-mobile {
|
||||
background-color: #9add03;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-icon-group-mobile {
|
||||
background-color: #588801;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
border-radius: 50%;
|
||||
max-width: 150px;
|
||||
border: 6px solid white;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
margin-top: 25px;
|
||||
min-height: 200px
|
||||
}
|
||||
|
||||
@media (min-width: 150px) and (max-width: 474px) {
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.5) translateY(-30%) translateX(0%);
|
||||
}
|
||||
|
||||
.socialicons-mobile {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 0px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 475px) and (max-width: 599px) {
|
||||
.socialicons-mobile {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 0px;
|
||||
margin-left: px;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.5) translateY(-30%) translateX(0%);
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 599px) and (max-width: 749px) {
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.5) translateY(25%) translateX(0%);
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
|
||||
width: 110%;
|
||||
margin-left: -3%;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 330px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 750px) and (max-width: 960px) {
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.8) translateY(25%) translateX(0%);
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
width: 110%;
|
||||
margin-left: -3%;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 425px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) and (max-width: 1280px) {
|
||||
.middle-col {
|
||||
margin-left: -30px;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -185px;
|
||||
min-width: 480px
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 270px;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.3) translateY(25%) translateX(-50px);
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 20px;
|
||||
margin-top: -6px
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
margin-left: 40px;
|
||||
margin-top: -8px
|
||||
}
|
||||
|
||||
.social-container {
|
||||
margin-left: 80px;
|
||||
min-width: 270px;
|
||||
max-height: 40px
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
margin-left: 50px;
|
||||
min-width: 250px;
|
||||
max-height: 35px
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 34px;
|
||||
max-width: 100px;
|
||||
margin-top: 15px;
|
||||
margin-left: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) and (max-width: 1600px) {
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.3) translateY(25%) translateX(-50px);
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -180px;
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 290px;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 20px;
|
||||
margin-top: -6px
|
||||
}
|
||||
|
||||
.social-container {
|
||||
margin-left: 90px;
|
||||
min-width: 350px;
|
||||
max-height: 40px
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
margin-left: 70px;
|
||||
margin-top: -8px
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
|
||||
margin-left: 40px;
|
||||
min-width: 290px;
|
||||
max-height: 35px
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 34px;
|
||||
max-width: 100px;
|
||||
margin-top: 15px;
|
||||
margin-left: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) and (max-width: 1919px) {
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.4) translateY(25%) translateX(-40px);
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -171px;
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 355px;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 1.5rem;
|
||||
margin-top: -4px;
|
||||
margin-left: 35px;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
font-size: 1.3rem;
|
||||
margin-left: 75px;
|
||||
margin-top: -2px
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
margin-left: 70px;
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) and (max-width: 2559px) {
|
||||
|
||||
.profile-container {
|
||||
margin-top: -160px;
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.4) translateY(25%) translateX(-40px);
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 1.5rem;
|
||||
margin-left: 35px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.social-container {
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
font-size: 1.3rem;
|
||||
margin-left: 110px;
|
||||
margin-top: -4px
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
margin-left: 30px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 38px;
|
||||
max-width: 100px;
|
||||
margin-top: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2560px) {
|
||||
.mobile-profile-picture-creator {
|
||||
transform: scale(1.5) translateY(25%) translateX(-20px);
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -150px
|
||||
}
|
||||
|
||||
.card-youtube {
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
font-size: 2rem;
|
||||
|
||||
}
|
||||
|
||||
.social-container {
|
||||
margin-left: 140px;
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
margin-left: 130px;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
font-size: 1.5rem;
|
||||
margin-left: 65px;
|
||||
margin-top: -6px
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 45px;
|
||||
max-width: 100px;
|
||||
margin-top: 15px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,383 +0,0 @@
|
||||
<template>
|
||||
|
||||
<v-row class="fluid" style="margin-top: -65px; position: relative; z-index: 0;">
|
||||
<v-col cols="12" class="pa-0" style="width: 100vw; overflow: hidden;">
|
||||
<v-img class="profile-banner" max-height="375" :src="imageSrc" cover
|
||||
style="box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);"></v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row style="background-color: #6b0065; height: 5px; box-shadow: rgba(0, 0, 0, 0.7);">
|
||||
</v-row>
|
||||
|
||||
<!-- "Core (Menu / Center / Donation)" -->
|
||||
<v-row>
|
||||
|
||||
<!-- Profile Info Picture name title & description -->
|
||||
<v-col class="middle-col" style="z-index: 5;" cols="12" xs="12" sm="12" md="6" lg="5" xl="4" xxl="4">
|
||||
<v-container class="profile-container">
|
||||
<v-container>
|
||||
<v-img :src="profilePicture" class="elevation-4 mobile-profile-picture-creator"></v-img>
|
||||
<!-- User informations Name & title -->
|
||||
<v-container class="background-profile-container" style="margin-top: -8%;">
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="7" offset="2" class="social-container">
|
||||
<h1 class="name-info">{{ name }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="height: 50px;" cols="6" offset="2" class="social-container2">
|
||||
<h1 class="occupation-info">{{ title }}</h1>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- User Social Network Links -->
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="7" style="margin-top: 1%; margin-bottom: -2%;">
|
||||
<v-row>
|
||||
<a href="https://www.facebook.com/MathieuCaronPro/">
|
||||
<img class="socialicons"
|
||||
src="/images/hutopymedia/icons/black/facebookblack.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@lesinterviewsatypiquesdema4692">
|
||||
<img class="socialicons" src="/images/hutopymedia/icons/black/youtube.png"
|
||||
alt="Description image 2">
|
||||
</a>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col background-color="primary"></v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Description -->
|
||||
<v-row>
|
||||
<v-container
|
||||
style="border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; margin-top: 15px;">
|
||||
<v-expansion-panels style="min-width: 320px;">
|
||||
<v-col cols="12" offset="-1">
|
||||
<v-expansion-panel class="background-pink text-justify"
|
||||
style="color: white;"
|
||||
text="Mathieu Caron est un intervieweur et un conférencier peu banal. Étant lui-même neuro-atypique, il concilie travail, entrevues et conférences avec l’objectif de démystifier le trouble de l’autisme ainsi que promouvoir la différence et le droit d’être atypique.
|
||||
|
||||
Il réalise des allocutions et de nombreuses entrevues avec des personnalités du monde artistique, politique et des affaires. C’est sur les médias sociaux, dont Facebook et YouTube, qu’il diffuse ces communications et rencontres inusitées. Son engagement et le dépassement de soi dans ses réalisations lui permettent de contribuer à élargir l’acceptation sociale de la différence et de la singularité."
|
||||
title="À propos des Entrevues Atypiques:">
|
||||
</v-expansion-panel>
|
||||
</v-col>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</v-container>
|
||||
<v-container>
|
||||
</v-container>
|
||||
|
||||
<!-- LES DENIS DROLETS -->
|
||||
<v-container style="margin-top: -30px">
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
ENTREVUE ATYPIQUE AVEC LES DENIS DROLETS</h1>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video LES DENIS DROLETS -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container>
|
||||
<iframe class="card-youtube" style="margin-left: 2.1%; width: 96%;"
|
||||
src="https://www.youtube.com/embed/bj4QTpzEO6E?si=9NrwEpsZ-f_KPsNo"
|
||||
title="Guillaumem" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen></iframe>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px">
|
||||
16-05-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">
|
||||
|
||||
Cette belle entrevue atypique est sans aucun doute un must dont j'en suis fiers !
|
||||
Office des personnes handicapées du Québec, sans vous cette vidéo n'aurait pas été
|
||||
possible.
|
||||
<br> <br>
|
||||
Merci aussi à Nissan et à J’achète Québécois, J’achète local ! JQJL !.
|
||||
Vidéo et montage par Jacob Lacroix.
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div
|
||||
style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- Jocelyn Grégoire -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
ENTREVUE ATYPIQUE AVEC JOCELYN GRÉGOIRE</h1>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container>
|
||||
<iframe class="card-youtube" style="margin-left: 2.1%; width: 96%;"
|
||||
src="https://www.youtube.com/embed//61ZNNeuQV_Y?si=Yl1lzDvuNkaAFdPU"
|
||||
title="Guillaumem" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen></iframe>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px">
|
||||
28-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">
|
||||
|
||||
Le 14 mars dernier, j'organisais un marathon d'entrevues atypiques au profit de
|
||||
Autisme Estrie,
|
||||
un organisme de mon coin dans le garage de Luc Poirier Perso.
|
||||
Ici, vous avez la dernière entrevue « atypique » réalisé avec Jocelyn Grégoire de
|
||||
Mordus
|
||||
d'immobilier
|
||||
<br> <br>
|
||||
Le montage de la vidéo a été réalisé par Marius Lome et le maquillage par Artiste
|
||||
maquilleuse
|
||||
Anick Bonhomme.
|
||||
<br> <br>
|
||||
Merci à mon assistant technique Éthienne Lafond-Lessard.
|
||||
À mes ''petits'' sponsors :
|
||||
Véronique Bibeau, Nicolas Jachète Québécois Beaudry, Nissan et Office des personnes
|
||||
handicapées
|
||||
du Québec.
|
||||
Musique : Lydie Morneau.
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div
|
||||
style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- Virginie Roy -->
|
||||
<v-container>
|
||||
<v-card class="flow-menu m-0 youtube-card">
|
||||
<!-- Title, date and btn -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<h1 class="card-title">
|
||||
ENTREVUE ATYPIQUE AVEC VIRGINIE ROY</h1>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</v-col>
|
||||
|
||||
<!-- Card core Video -->
|
||||
<div style="background-color: rgba(255, 255, 255, 0.1); margin-left: -5%; margin-right: -5%;">
|
||||
<v-container>
|
||||
<iframe class="card-youtube" style="margin-left: 2.1%; width: 96%;"
|
||||
src="https://www.youtube.com/embed/dYZZCDVUcuQ?si=g_97RA0w9Vs5hl13"
|
||||
title="Guillaumem" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen></iframe>
|
||||
|
||||
<!-- Date -->
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<h1 style=" margin-right: 40px; margin-top: 10px">
|
||||
28-04-2024
|
||||
</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
<p class="text-justify pa-10" style="margin-bottom: -6%; font-size: 1em">
|
||||
|
||||
Le 14 mars dernier, j'organisais un marathon d'entrevues atypiques au profit de
|
||||
Autisme Estrie,
|
||||
un organisme de mon coin dans le garage de Luc Poirier Perso.
|
||||
Ici, vous avez la dernière entrevue « atypique » réalisé avec Jocelyn Grégoire de
|
||||
Mordus
|
||||
d'immobilier
|
||||
<br> <br>
|
||||
Le montage de la vidéo a été réalisé par Marius Lome et le maquillage par Artiste
|
||||
maquilleuse
|
||||
Anick Bonhomme.
|
||||
<br> <br>
|
||||
Merci à mon assistant technique Éthienne Lafond-Lessard.
|
||||
À mes ''petits'' sponsors :
|
||||
Véronique Bibeau, Nicolas Jachète Québécois Beaudry, Nissan et Office des personnes
|
||||
handicapées
|
||||
du Québec.
|
||||
Musique : Lydie Morneau.
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Like dislike commment Section -->
|
||||
<div style="height: 20px;"></div>
|
||||
<div
|
||||
style="z-index: 500; margin-bottom: 2%; background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
<div
|
||||
style="background-color: rgba(0, 0, 0, 1); height: 3px; width: 100%; margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<v-text-field style="margin-left: 2%; margin-bottom: 15px; margin-bottom: -20px"
|
||||
placeholder="Commentaire (Arrive bientôt)"></v-text-field>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
let imageSrc = '/images/usersmedia/mathieuCaron/banners/bannerMathieuCaron01.png';
|
||||
let profilePicture = '/images/usersmedia/mathieuCaron/profilepictures/profileMathieuCaron01.png';
|
||||
let name = 'Mathieu Caron'
|
||||
let title = 'Intervieweur'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.name-info {
|
||||
margin-top: -10px;
|
||||
margin-left: 15%;
|
||||
}
|
||||
|
||||
.occupation-info {
|
||||
margin-left: 15%;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
margin-top: -16%;
|
||||
}
|
||||
|
||||
.background-profile-container {
|
||||
background-color: #ececec;
|
||||
color: white;
|
||||
border-top: 3px solid #a30e79;
|
||||
border-right: 3px solid #a30e79;
|
||||
font-weight: 700;
|
||||
font-size: 1.15rem;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3)
|
||||
}
|
||||
|
||||
.background-pink {
|
||||
background-color: #1d1d1b;
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.socialicons {
|
||||
width: 35px;
|
||||
max-width: 100px;
|
||||
margin-top: 3px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.youtube-card {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
border-radius: 15px;
|
||||
background-color: #f4f4f4;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4)
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.social-container {
|
||||
background-color: #101b49;
|
||||
border-top-right-radius: 30px;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container2 {
|
||||
background-color: #698fe7;
|
||||
border-bottom-right-radius: 30px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mobile-profile-picture-creator {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
border-radius: 50%;
|
||||
max-width: 150px;
|
||||
border: 6px solid white;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
margin-top: 25px;
|
||||
min-height: 200px
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,21 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-column">
|
||||
<div class="flex flex-row p-1 items-center">
|
||||
<div class="px-2 content-center">
|
||||
<img :src="message.portrait"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="32px"
|
||||
height="32px">
|
||||
</div>
|
||||
|
||||
<div class="p-2 flex-1">
|
||||
<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>
|
||||
<span class="font-semibold font-sans mr-2">{{ message.createdBy }}</span>
|
||||
<span class="text-sm font-sans text-gray-700">il y a 3 heures</span>
|
||||
<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 :location="location">
|
||||
<v-menu class="ml-auto">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn variant="plain" icon v-bind="props">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
@@ -35,42 +47,107 @@
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div class="font-sans">
|
||||
{{ message.value }}
|
||||
<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="acceptechanges"
|
||||
></v-textarea>
|
||||
|
||||
<div class="flex flex-col px-2 space-y-1">
|
||||
<v-btn variant="plain" @click="acceptechanges">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn variant="plain" @click="cancel">
|
||||
<v-icon class="rounded-full">mdi-cancel</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editMessage(message) {
|
||||
// Logic for editing the message
|
||||
console.log('Edit message', message);
|
||||
},
|
||||
deleteMessage(message) {
|
||||
// Logic for deleting the message
|
||||
console.log('Delete message', message);
|
||||
},
|
||||
reportMessage(message) {
|
||||
// Logic for reporting the message
|
||||
console.log('Report message', message);
|
||||
}
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { time_ago } from "@/internal_time_ago.js";
|
||||
|
||||
const isEditMessage = ref(false);
|
||||
const editMessageValue = ref("");
|
||||
const originalMessageValue = ref("");
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function editMessage(message) {
|
||||
isEditMessage.value = true;
|
||||
originalMessageValue.value = message.value;
|
||||
editMessageValue.value = message.value;
|
||||
}
|
||||
|
||||
function acceptechanges() {
|
||||
props.message.value = editMessageValue.value;
|
||||
isEditMessage.value = false;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
editMessageValue.value = originalMessageValue.value;
|
||||
isEditMessage.value = false;
|
||||
}
|
||||
|
||||
function deleteMessage(message) {
|
||||
console.log('Delete message', message);
|
||||
}
|
||||
|
||||
function reportMessage(message) {
|
||||
console.log('Report message', message);
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
watch(isEditMessage, (newValue) => {
|
||||
if (newValue) {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
} else {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (isEditMessage.value) {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.content-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,90 @@
|
||||
<template>
|
||||
<v-infinite-scroll
|
||||
:items="messages"
|
||||
:onLoad="fetchMessages"
|
||||
mode="manual"
|
||||
class="justify-items-center"
|
||||
>
|
||||
<template v-for="message in messages" :key="message">
|
||||
<message :message="message"
|
||||
class="border-b"
|
||||
></message>
|
||||
</template>
|
||||
|
||||
<div v-for="(message, index) in messages" :key="index">
|
||||
<Message :message="message"></Message>
|
||||
</div>
|
||||
<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 Message from "@/views/messages/Message.vue";
|
||||
// import posts from "@/views/posts/posts.json";
|
||||
|
||||
import {ref, onBeforeMount} from 'vue';
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {defineProps, onBeforeMount, ref} from 'vue';
|
||||
import Message from "@/views/messages/Message.vue";
|
||||
|
||||
const props = defineProps({
|
||||
contentId: {
|
||||
subjectId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const errorMessage = ref(null);
|
||||
let last_id = null;
|
||||
const client = useClient();
|
||||
const messages = ref();
|
||||
const messages = ref(props.messages);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
if (props.contentId == null) return
|
||||
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 {
|
||||
const response = await client.get(`/api/messages/${props.contentId}`)
|
||||
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) {
|
||||
messages.value = response.data
|
||||
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');
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -1,61 +1,134 @@
|
||||
<template>
|
||||
|
||||
<div class="flex flex-column">
|
||||
<div class="flex flex-row items-center ">
|
||||
<img :src="userStore.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="Votre commentaire..."
|
||||
hide-details
|
||||
auto-grow
|
||||
rows="1"
|
||||
maxlength="1024"
|
||||
class="pr-1 ml-6 flex-grow"
|
||||
@keydown.enter.prevent="publish"
|
||||
|
||||
<div class="flex flex-row">
|
||||
|
||||
<div class="px-1 content-center">
|
||||
<img src="/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png"
|
||||
alt="Profile Image"
|
||||
class="rounded-full"
|
||||
width="32px"
|
||||
height="32px">
|
||||
>
|
||||
</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>
|
||||
|
||||
<v-text-field
|
||||
v-model="message"
|
||||
density="compact"
|
||||
variant="solo-inverted"
|
||||
placeholder="Votre commentaire..."
|
||||
hide-details
|
||||
clearable>
|
||||
</v-text-field>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 p-2 justify-end">
|
||||
<v-btn style="border-radius: 20px" variant="text">Annuler</v-btn>
|
||||
<v-btn style="border-radius: 20px" @click="publish">Ajouter un commentaire</v-btn>
|
||||
<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>
|
||||
|
||||
|
||||
<v-dialog v-model="loginModal" 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>Vous devez être connecté pour poster un commentaire.</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>
|
||||
|
||||
<script setup>
|
||||
// import posts from "@/views/posts/posts.json";
|
||||
import {ref} from 'vue'
|
||||
import {v7} from 'uuid'
|
||||
import {useClient} from '@/plugins/api.js'
|
||||
import {useUserStore} from "@/stores/userStore.js"
|
||||
import {useAuthStore} from "@/stores/authStore.js"
|
||||
import LoginForm from "@/views/main/LoginForm.vue"
|
||||
|
||||
import {useClient} from '@/plugins/api.js';
|
||||
import {defineProps, ref} from 'vue';
|
||||
const closeModal = () => {
|
||||
loginModal.value = false;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
contentId: {
|
||||
subjectId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const client = useClient();
|
||||
const message = ref("");
|
||||
const emits = defineEmits(['message-posted'])
|
||||
|
||||
const loginModal = ref(false);
|
||||
const client = useClient()
|
||||
const value = ref("")
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const publish = async () => {
|
||||
await client.post(
|
||||
`/api/messages/`,
|
||||
{
|
||||
"contentId": props.contentId,
|
||||
"message": message.value
|
||||
});
|
||||
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": userStore.user.id,
|
||||
"createdByName": userStore.alias,
|
||||
"createdByPortraitUrl": userStore.portraitUrl,
|
||||
"createdAt": new Date(Date.now()).toISOString(),
|
||||
"value": value.value,
|
||||
"parentId": null
|
||||
})
|
||||
value.value = ''
|
||||
} catch (error) {
|
||||
console.error(`post api/message : ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSuccess = () => {
|
||||
loginModal.value = false;
|
||||
}
|
||||
|
||||
const handleFailure = () => {
|
||||
console.error('Login failed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-red-500 {
|
||||
color: #f56565;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[
|
||||
{
|
||||
"content": "40C28A05-2262-43F4-862A-09A4246A8DDE",
|
||||
"portrait": "/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png",
|
||||
"created_by": "@marchy",
|
||||
"created_at": "",
|
||||
"message": "Hello World!",
|
||||
"thumb_up_count": 0,
|
||||
"thumb_down_count": 0
|
||||
},
|
||||
{
|
||||
"content": "40C28A05-2262-43F4-862A-09A4246A8DDE",
|
||||
"portrait": "/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png",
|
||||
"created_by": "@marchy",
|
||||
"created_at": "",
|
||||
"message": "Welcome to life!",
|
||||
"thumb_up_count": 0,
|
||||
"thumb_down_count": 3
|
||||
},
|
||||
{
|
||||
"content": "40C28A05-2262-43F4-862A-09A4246A8DDE",
|
||||
"portrait": "/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png",
|
||||
"created_by": "@marchy",
|
||||
"created_at": "",
|
||||
"message": "We love you!",
|
||||
"thumb_up_count": 1,
|
||||
"thumb_down_count": 0
|
||||
}
|
||||
]
|
||||
166
src/views/profile/ProfilePage.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
|
||||
<!-- Left Menu -->
|
||||
<div class="bg-[#f4f4f4] 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 bg-[#f4f4f4]">
|
||||
<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">Gérer votre compte Hutopy</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 bg-[#f4f4f4] 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>
|
||||
Créateur
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="text" @click="currentComponent = 'PersonnalInfo'">
|
||||
<v-icon class="mr-2">mdi-information</v-icon>
|
||||
Utilisateur
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="text" @click="currentComponent = 'ManageAccount'">
|
||||
<v-icon class="mr-2">mdi-account</v-icon>
|
||||
Gestion de Comptes
|
||||
</v-btn>
|
||||
|
||||
<v-btn variant="text" @click="currentComponent = 'AccountSecurity'">
|
||||
<v-icon class="mr-2">mdi-security</v-icon>
|
||||
Sécurité
|
||||
</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 === 'ManageAccount'">
|
||||
<manage-account></manage-account>
|
||||
</template>
|
||||
<template v-else-if="currentComponent === 'CreatorPage'">
|
||||
<creator-page></creator-page>
|
||||
</template>
|
||||
<template v-else-if="currentComponent === 'PersonnalInfo'">
|
||||
<personnal-info></personnal-info>
|
||||
</template>
|
||||
<template v-else-if="currentComponent === 'AccountSecurity'">
|
||||
<account-security></account-security>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from "vue";
|
||||
import ManageAccount from "@/views/profile/management/ManageAccount.vue";
|
||||
import CreatorPage from "@/views/profile/creators/CreatorPage.vue";
|
||||
import PersonnalInfo from "@/views/profile/account/PersonnalInfo.vue";
|
||||
import AccountSecurity from "@/views/profile/security/AccountSecurity.vue";
|
||||
|
||||
const currentComponent = ref('PersonnalInfo'); // Default component
|
||||
const isDown = ref(false);
|
||||
const startX = ref(0);
|
||||
const scrollLeft = ref(0);
|
||||
|
||||
onMounted(() => {
|
||||
const slider = document.querySelector('.custom-scroll');
|
||||
slider.addEventListener('mousedown', (e) => {
|
||||
isDown.value = true;
|
||||
slider.classList.add('active');
|
||||
startX.value = e.pageX - slider.offsetLeft;
|
||||
scrollLeft.value = slider.scrollLeft;
|
||||
});
|
||||
|
||||
slider.addEventListener('mouseleave', () => {
|
||||
isDown.value = false;
|
||||
slider.classList.remove('active');
|
||||
});
|
||||
|
||||
slider.addEventListener('mouseup', () => {
|
||||
isDown.value = false;
|
||||
slider.classList.remove('active');
|
||||
});
|
||||
|
||||
slider.addEventListener('mousemove', (e) => {
|
||||
if (!isDown.value) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - slider.offsetLeft;
|
||||
const walk = (x - startX.value) * 3; // scroll-fast
|
||||
slider.scrollLeft = scrollLeft.value - walk;
|
||||
});
|
||||
});
|
||||
|
||||
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>
|
||||
23
src/views/profile/account/AdressesHome.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Adresse à votre domicile</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="homeAddress"
|
||||
label="Votre adresse à votre domicile"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" >Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const homeAddress = ref('');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
23
src/views/profile/account/AdressesWork.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Adresse au travail</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="homeAddress"
|
||||
label="Votre adresse au travail"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" >Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const homeAddress = ref('');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
26
src/views/profile/account/Birthday.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Date de naissance</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="date"
|
||||
label="AAAA-MM-JJ"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain" color="black" >Annuler</v-btn>
|
||||
<v-btn color="#A6147D">Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
const date = ref('');
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
22
src/views/profile/account/Email.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Courriel</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="email"
|
||||
label="Votre courriel"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain" color="black">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" >Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const email = ref('');
|
||||
</script>
|
||||
|
||||
|
||||
33
src/views/profile/account/Gender.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Genre</div>
|
||||
|
||||
<v-radio-group v-model="gender" row>
|
||||
<v-radio label="Homme" value="Homme"></v-radio>
|
||||
<v-radio label="Femme" value="Femme"></v-radio>
|
||||
<v-radio label="Autre" value="Autre"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<v-text-field
|
||||
v-if="gender === 'Autre'"
|
||||
v-model="otherGender"
|
||||
label="Veuillez spécifier"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain" color="black">Annuler</v-btn>
|
||||
<v-btn color="#A6147D">Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const gender = ref('');
|
||||
const otherGender = ref('');
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
50
src/views/profile/account/Name.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Nom</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="name"
|
||||
label="Name"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="lastName"
|
||||
label="Last Name"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="displayName"
|
||||
label="Display Name as"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain" color="black" @click="handleCancel">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" >Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const name = ref('');
|
||||
const lastName = ref('');
|
||||
const displayName = ref('');
|
||||
|
||||
|
||||
|
||||
const handleCancel = () => {
|
||||
// Logic for canceling the changes
|
||||
name.value = '';
|
||||
lastName.value = '';
|
||||
displayName.value = '';
|
||||
console.log('Cancelled');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
166
src/views/profile/account/PersonnalInfo.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<h1 class="uppercase pb-5 text-2xl">
|
||||
<v-icon class="mr-2">mdi-information</v-icon> Informations personnelles
|
||||
</h1>
|
||||
|
||||
<div class="border rounded-2xl w-full max-w-[800px]">
|
||||
<div class="py-5 uppercase ml-4">Information de base</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"
|
||||
@click="openModal('Name')">
|
||||
<span class="min-w-40 text-left">Photo de profil</span>
|
||||
<span class="flex-auto pr-6 text-left">Une photo de profil aide à personnaliser votre compte</span>
|
||||
<span class="flex-none">
|
||||
<img
|
||||
:src="'/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'"
|
||||
alt="Profile Image"
|
||||
class="ml-2 rounded-full"
|
||||
style="width: 48px; height: 48px;">
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<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"
|
||||
@click="openModal('Name')">
|
||||
<span class="pa-2 min-w-40 text-left">Nom</span>
|
||||
<span class="flex-auto text-left pr-6">Pascal Marchesseault</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<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"
|
||||
@click="openModal('Birthday')">
|
||||
<span class="flex-none pa-2 min-w-40 text-left">Date de naissance</span>
|
||||
<span class="flex-auto text-left pr-6">27 octobre 1988</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out rounded-b-2xl w-full"
|
||||
@click="openModal('Gender')">
|
||||
<span class="flex-none pa-2 min-w-40 text-left">Genre</span>
|
||||
<span class="flex-auto text-left pr-6">Homme</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone & email -->
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
<div class="border rounded-2xl w-full max-w-[800px]">
|
||||
<div class="py-5 uppercase ml-4">Coordonnées</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"
|
||||
@click="openModal('Email')">
|
||||
<span class="min-w-40 text-left">Courriel</span>
|
||||
<span class="flex-auto pr-6 text-left">marchesseault_pascal@hotmail.com</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<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 rounded-b-2xl"
|
||||
@click="openModal('Phone')">
|
||||
<span class="pa-2 min-w-40 text-left">Téléphone</span>
|
||||
<span class="flex-auto text-left pr-6">(581) 999-7540</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
<div class="border rounded-2xl w-full max-w-[800px]">
|
||||
<div class="py-5 uppercase ml-4">Adresses</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"
|
||||
@click="openModal('AdressesHome')">
|
||||
<span class="min-w-40 text-left">Domicile</span>
|
||||
<span class="flex-auto pr-6 text-left">2127 Rue De Casson, Trois-Rivières, Qc</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<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 rounded-b-2xl"
|
||||
@click="openModal('AdressesWork')">
|
||||
<span class="pa-2 min-w-40 text-left">Travail</span>
|
||||
<span class="flex-auto pr-6 text-left">2127 Rue De Casson, Trois-Rivières, Qc</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<component :is="currentComponent"></component>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Name from './Name.vue';
|
||||
import AdressesHome from './AdressesHome.vue';
|
||||
import AdressesWork from './AdressesWork.vue';
|
||||
import Birthday from './Birthday.vue';
|
||||
import Email from './Email.vue';
|
||||
import Gender from './Gender.vue';
|
||||
import Phone from './Phone.vue';
|
||||
|
||||
const dialog = ref(false);
|
||||
const currentComponent = ref('');
|
||||
|
||||
const componentsMap = {
|
||||
Name,
|
||||
AdressesHome,
|
||||
AdressesWork,
|
||||
Birthday,
|
||||
Email,
|
||||
Gender,
|
||||
Phone
|
||||
};
|
||||
|
||||
const openModal = (component) => {
|
||||
currentComponent.value = componentsMap[component];
|
||||
dialog.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.HoverBtn:hover {
|
||||
@apply bg-[#A6147D] text-white;
|
||||
@apply hover:opacity-90; /* Réduire l'opacité au survol */
|
||||
}
|
||||
</style>
|
||||
32
src/views/profile/account/Phone.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">Numéro de téléphone</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="cellNumber"
|
||||
label="Numéro de cellulaire"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="workNumber"
|
||||
label="Numéro au travail"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<v-btn variant="plain" color="black">Annuler</v-btn>
|
||||
<v-btn color="#A6147D" >Enregistrer</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const cellNumber = ref('');
|
||||
const workNumber = ref('');
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
67
src/views/profile/creators/About.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<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.about.title)
|
||||
const description = ref(props.creator.about.description)
|
||||
|
||||
const client = useClient()
|
||||
const save = async () => {
|
||||
try {
|
||||
await client.post(
|
||||
`/api/creators/${props.creator.id}/about`,
|
||||
{
|
||||
"title": title.value || null,
|
||||
"description": description.value || null
|
||||
})
|
||||
|
||||
props.creator.about.title = title
|
||||
props.creator.about.description = description
|
||||
|
||||
emits('closeRequested')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pb-5 text-2xl">About</div>
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="title"
|
||||
label="Titre"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-row align-center">
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="description"
|
||||
label="Description"
|
||||
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>
|
||||
75
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"
|
||||
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 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>
|
||||
161
src/views/profile/creators/ColorsPicker.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup>
|
||||
import {ref, computed} from 'vue';
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const props = defineProps({
|
||||
creator: {
|
||||
required: true
|
||||
},
|
||||
colorName: {
|
||||
type: String,
|
||||
default: 'bannerTop'
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['closeRequested']);
|
||||
|
||||
const bannerTopColor = ref(props.creator.colors.bannerTop)
|
||||
const bannerBottomColor = ref(props.creator.colors.bannerBottom)
|
||||
const accentColor = ref(props.creator.colors.accent)
|
||||
const menuColor = ref(props.creator.colors.menu)
|
||||
|
||||
const selectedColorName = ref(props.colorName);
|
||||
const selectedColor = computed({
|
||||
get() {
|
||||
switch (selectedColorName.value) {
|
||||
case 'bannerTop':
|
||||
return bannerTopColor.value;
|
||||
case 'bannerBottom':
|
||||
return bannerBottomColor.value;
|
||||
case 'accent':
|
||||
return accentColor.value;
|
||||
case 'menu':
|
||||
return menuColor.value;
|
||||
default:
|
||||
return bannerTopColor.value;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
switch (selectedColorName.value) {
|
||||
case 'bannerTop':
|
||||
bannerTopColor.value = value;
|
||||
break;
|
||||
case 'bannerBottom':
|
||||
bannerBottomColor.value = value;
|
||||
break;
|
||||
case 'accent':
|
||||
accentColor.value = value;
|
||||
break;
|
||||
case 'menu':
|
||||
menuColor.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`,
|
||||
{
|
||||
"bannerTop": bannerTopColor.value || null,
|
||||
"bannerBottom": bannerBottomColor.value || null,
|
||||
"accent": accentColor.value || null,
|
||||
"menu": menuColor.value || null
|
||||
});
|
||||
|
||||
props.creator.colors.bannerTop = bannerTopColor.value;
|
||||
props.creator.colors.bannerBottom = bannerBottomColor.value;
|
||||
props.creator.colors.accent = accentColor.value;
|
||||
props.creator.colors.menu = menuColor.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 gap-6 justify-center items-start mt-5">
|
||||
|
||||
<div class="grid grid-cols-2 grid-rows-2 gap-4">
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'bannerTop' }"
|
||||
:style="{ backgroundColor: bannerTopColor }"
|
||||
@click="selectColor('bannerTop')"
|
||||
>
|
||||
<span class="color-name">Haut Banniere</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'bannerBottom' }"
|
||||
:style="{ backgroundColor: bannerBottomColor }"
|
||||
@click="selectColor('bannerBottom')"
|
||||
>
|
||||
<span class="color-name">Bas Banniere</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'accent' }"
|
||||
:style="{ backgroundColor: accentColor }"
|
||||
@click="selectColor('accent')"
|
||||
>
|
||||
<span class="color-name">Accent</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="color-square"
|
||||
:class="{ selected: selectedColorName === 'menu' }"
|
||||
:style="{ backgroundColor: menuColor }"
|
||||
@click="selectColor('menu')"
|
||||
>
|
||||
<span class="color-name">Menu</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-px bg-gray-300 h-full"></div>
|
||||
|
||||
<div class="flex-grow">
|
||||
<v-color-picker v-model="selectedColor"></v-color-picker>
|
||||
</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-white;
|
||||
}
|
||||
|
||||
.color-square.selected {
|
||||
@apply border-4 border-solid border-[crimson];
|
||||
}
|
||||
|
||||
.color-name {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
52
src/views/profile/creators/CreateCreator.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
|
||||
const emits = defineEmits(['requestAccept', 'requestCancel'])
|
||||
|
||||
const creatorName = ref('')
|
||||
|
||||
function requestCancel() {
|
||||
emits('requestCancel')
|
||||
}
|
||||
|
||||
function requestAccept() {
|
||||
emits('requestAccept', creatorName.value)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<div class="py-2 text-2xl font-bold flex flex-row items-center">
|
||||
<div class="flex-grow text-center">Devenez Créateur</div>
|
||||
<v-btn icon @click="requestCancel" class="ml-auto" variant="text">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
variant="outlined"
|
||||
v-model="creatorName"
|
||||
label="Nom de créateur"
|
||||
outlined
|
||||
></v-text-field>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<v-btn
|
||||
variant="flat"
|
||||
@click="requestCancel"
|
||||
:style="{ borderColor: 'rgb(159, 76, 173)', color: 'rgb(159, 76, 173)' }"
|
||||
>
|
||||
Annuler
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="requestAccept"
|
||||
:style="{ borderColor: 'rgb(159, 76, 173)', color: 'rgb(159, 76, 173)' }"
|
||||
>
|
||||
Créer
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
338
src/views/profile/creators/CreatorPage.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<script setup>
|
||||
import {computed, ref} from 'vue'
|
||||
import {useUserStore} from "@/stores/userStore.js"
|
||||
import Socials from './Socials.vue'
|
||||
import BannerPicker from './BannerPicker.vue'
|
||||
import ColorsPicker from './ColorsPicker.vue'
|
||||
import LogoPicker from "./LogoPicker.vue"
|
||||
import About from "./About.vue";
|
||||
import CreateCreator from "./CreateCreator.vue";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const colorBannerTop = computed(() => userStore.creator.colors.bannerTop || '#a0c0f0')
|
||||
const colorBannerBottom = computed(() => userStore.creator.colors.bannerBottom || '#a0c0f0')
|
||||
const colorAccent = computed(() => userStore.creator.colors.accent || '#a0c0f0')
|
||||
const colorMenu = computed(() => userStore.creator.colors.menu || '#a0c0f0')
|
||||
const imageBanner = computed(() => userStore.creator.images.banner || '/images/placeholders/banner.png')
|
||||
const imageLogo = computed(() => userStore.creator.images.logo || '/images/placeholders/logo.png')
|
||||
|
||||
const dialog = ref(false);
|
||||
const currentComponent = ref('')
|
||||
const colorToEdit = ref(null)
|
||||
|
||||
const componentsMap = {
|
||||
BannerPicker,
|
||||
LogoPicker,
|
||||
Socials,
|
||||
ColorsPicker,
|
||||
About,
|
||||
CreateCreator
|
||||
};
|
||||
|
||||
async function requestAccept(creatorName) {
|
||||
const client = useClient()
|
||||
const response = await client.post(
|
||||
'/api/creators',
|
||||
{
|
||||
'creatorId': userStore.user.id,
|
||||
'name': creatorName
|
||||
})
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
await userStore.fetchCurrentUserProfile()
|
||||
dialog.value = false
|
||||
} else {
|
||||
console.log(`An issue while creating the creator: ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
function requestCancel() {
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
const openDialog = (component, colorName = null) => {
|
||||
currentComponent.value = componentsMap[component]
|
||||
colorToEdit.value = colorName
|
||||
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="userStore.creator"
|
||||
:colorName="colorToEdit"
|
||||
@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>
|
||||
Informations de votre page
|
||||
</h1>
|
||||
|
||||
<div v-if="userStore.hasCreator" class="w-full max-w-[800px]">
|
||||
|
||||
<div class="my-10 border rounded-2xl">
|
||||
|
||||
<div class="py-5 uppercase ml-4">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">Nom</span>
|
||||
<span class="flex-auto text-left pr-6 capitalize">{{ userStore.creator.name }}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('About')"
|
||||
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">Titre</span>
|
||||
<span class="flex-auto text-left pr-6">{{ userStore.creator.about.title }}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('About')"
|
||||
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-2xl ">
|
||||
<span class="pa-2 min-w-32 text-left">Description</span>
|
||||
<span class="flex-auto text-left pr-6">{{ userStore.creator.about.description }}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-2xl">
|
||||
<div class="py-5 uppercase ml-4">Bannière et image de profil</div>
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
<button
|
||||
@click="openDialog('ColorsPicker', 'bannerTop')"
|
||||
class="flex justify-end h-10 align-center text-white px-5 hover:brightness-150"
|
||||
:style="{ backgroundColor: colorBannerTop }"
|
||||
>
|
||||
<v-icon>mdi-eyedropper-variant</v-icon>
|
||||
</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
|
||||
@click="openDialog('ColorsPicker', 'bannerBottom')"
|
||||
class="flex justify-end h-10 align-center text-white px-5 hover:brightness-150"
|
||||
:style="{ backgroundColor: colorBannerBottom }"
|
||||
>
|
||||
<v-icon>mdi-eyedropper-variant</v-icon>
|
||||
</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"
|
||||
:style="{ borderColor: colorAccent }"
|
||||
:src="imageLogo"
|
||||
alt="Profile Image"
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('ColorsPicker', 'accent')"
|
||||
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-circle-outline</v-icon></span>
|
||||
<span class="flex-auto text-left pr-6">Couleur du contour de la photo de profil.</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-eyedropper-variant</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('ColorsPicker', 'menu')"
|
||||
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-2xl">
|
||||
<span class="flex-none pa-2 min-w-32 text-left"> <v-icon>mdi-menu</v-icon></span>
|
||||
<span class="flex-auto text-left pr-6">Couleur des entêtes de menus</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-eyedropper-variant</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 border rounded-2xl">
|
||||
|
||||
<div class="py-5 uppercase ml-4">Réseaux Sociaux</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">{{ userStore.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">{{ userStore.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 min-w-32 text-left"> <img src="/images/hutopymedia/icons/x.svg" width="23px"
|
||||
height="23px"></span>
|
||||
<span class="flex-auto text-left pr-6">{{ userStore.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">{{ userStore.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">
|
||||
<img src="/images/hutopymedia/icons/tiktok.svg" class="w-5 h-5">
|
||||
</span>
|
||||
<span class="flex-auto text-left pr-6">{{ userStore.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">{{ userStore.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">{{ userStore.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-2xl ">
|
||||
<span class="pa-2 min-w-32 text-left"><v-icon>mdi-web</v-icon></span>
|
||||
<span class="flex-auto text-left pr-6">{{ userStore.creator.socials.websiteUrl }}</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-full max-w-[800px]">
|
||||
<div class="my-10 border rounded-2xl">
|
||||
|
||||
<div class="py-5 uppercase ml-4 px-4">Informations pour pour votre page</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<button
|
||||
@click="openDialog('CreateCreator')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out">
|
||||
<span class="flex-none pa-2 min-w-32 text-left">Nom (Unique) </span>
|
||||
<span class="flex-auto text-left pr-6">Vous devez vous assurer de choisir un nom unique ou d'utiliser votre propre nom. Cela est important car c'est votre nom de marque, celui que vous allez présenter aux gens. Par exemple, notre projet s'appelle Hutopy, nous avons donc utilisé ce nom pour notre page officielle sur la plateforme.</span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('CreateCreator')"
|
||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out">
|
||||
<span class="flex-none pa-2 min-w-32 text-left">Titre</span>
|
||||
<span class="flex-auto text-left pr-6">Le titre doit refléter ce que vous souhaitez mettre en avant sur la plateforme, ce que vous voulez présenter comme contenu ou la raison de votre présence sur la plateforme. Par exemple, des artistes pourraient écrire chanteur, danseur, peintre ou autre. Une entreprise pourrait indiquer son activité. Dans notre cas, pour Hutopy, c'est 'Créer un monde meilleur'. </span>
|
||||
<span class="flex-none">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openDialog('CreateCreator')"
|
||||
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-2xl ">
|
||||
<span class="pa-2 min-w-32 text-left">Description</span>
|
||||
<span class="flex-auto text-left pr-6">Pour la description, c'est un espace où vous pouvez écrire ce que vous souhaitez : décrire votre page, le service que vous offrez, la raison de votre présence sur la plateforme, votre historique... Vous choisissez ! Dans notre cas, pour Hutopy, nous sommes ici afin de créer une plateforme sur laquelle les gens pourront s'exprimer et créer, tout en étant respectés et non exploités. </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;
|
||||
/* Réduire l'opacité au survol */
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
77
src/views/profile/creators/LogoPicker.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<h2 class="text-2xl font-semibold mb-4 flex justify-center">
|
||||
Logo
|
||||
</h2>
|
||||
|
||||
<div class="flex justify-center mb-5">
|
||||
<img
|
||||
:src="fileUrl"
|
||||
class="w-full transition duration-200 ease-in-out transform max-w-[400px]"
|
||||
alt="Aperçu de la bannière"
|
||||
/>
|
||||
</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 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.logoUrl = fileUrl
|
||||
emits('closeRequested')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
emits('closeRequested')
|
||||
}
|
||||
|
||||
</script>
|
||||
146
src/views/profile/creators/Socials.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
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>
|
||||
|
||||
<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">
|
||||
<img src="/images/hutopymedia/icons/tiktok.svg" width="23px" height="23px" class="mb-5 mr-2">
|
||||
<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">
|
||||
<img src="/images/hutopymedia/icons/x.svg" width="23px" height="23px" class="mb-5 mr-2">
|
||||
<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>
|
||||