Merged PR 102: Main-Future into main

This commit is contained in:
Dominic Villemure
2024-08-20 00:59:17 +00:00
106 changed files with 5643 additions and 4034 deletions

4
.env.development Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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>

View File

@@ -1,3 +0,0 @@
import { ref } from 'vue';
export const eventBus = ref({});

64
src/internal_time_ago.js Normal file
View 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;
}

View File

@@ -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');

View File

@@ -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;
}
}

View File

@@ -1,11 +0,0 @@
export default class ProfileColorsModel
{
bannerTop = "";
bannerBottom = "";
accent = "";
menu = "";
static createFromApiResult(apiResult){
return Object.assign(new ProfileColorsModel(), apiResult)
}
}

View File

@@ -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)
}
}

View File

@@ -1,10 +0,0 @@
export default class StoredDataUrlsModel
{
bannerPictureUrl = "";
profilePictureUrl = "";
websiteIconUrl = "";
static createFromApiResult(apiResult){
return Object.assign(new StoredDataUrlsModel(), apiResult)
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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
View 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}
})

View File

@@ -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 }
})

View 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}
})

View 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}
});

View File

@@ -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
View 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}
})

View File

@@ -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>
<login-form :onSuccess="handleSuccess"></login-form>
</div>
</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>

View File

@@ -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">
<v-carousel
hide-delimiters
v-if="hasUrls"
:show-arrows="props.content.urls.length > 1"
:show-indicators="props.content.urls.length > 1"
>
<v-carousel-item
v-for="url in props.content.urls"
:key="url"
class="image-container"
@click="redirectToContent"
>
<component :is="getComponent(url)" :src="url"></component>
</v-carousel-item>
</v-carousel>
</div>
<div class="h-48 object-cover bg-purple">
<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>
<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>
<donation-button :color-border="colorMenu"
:color-accent="colorAccent"
:creator-id="creatorId"
:creator-name="creatorName"
:creator-logo="creatorLogo"
iconColorClass="text-black"
></donation-button>
</div>
<router-link :to="'content/' + props?.content?.id">
<div class="text-lg font-bold">{{ props.content.title }}</div>
<div class="text-sm text-gray-500">{{ props.content.description }}</div>
</router-link>
<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>

View File

@@ -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 () => {
if (props.creatorId == null) return
try {
const response = await client.get(`/api/contents/user/${props.creatorId}`)
if (response.status >= 200 && response.status < 300) {
contents.value = response.data
console.table(contents.value)
async function onContentDeleted(contentId) {
contents.value = contents.value.filter(c => c.id !== contentId)
}
} catch (error) {
console.error("Failed to fetch posts", error);
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 {
let uri = `/api/contents/creator/${props.creatorId}?page_size=${page_size}`
if (last_id !== null) uri = uri + `&last_id=${last_id}`
const response = await client.get(uri)
if (response.status >= 200 && response.status < 300) {
const contentCount = response.data.length
if (contentCount > 0) {
contents.value.push(...response.data)
const [last_content] = response.data.slice(-1)
last_id = last_content.id
}
if (contentCount < page_size)
done('empty')
else
done('ok')
}
} catch (error) {
console.error("Failed to fetch posts", error);
errorMessage.value = error.message || "Failed to fetch contents";
done('error')
}
}
</script>

View File

@@ -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>
<!-- back Btn -->
<div class="absolute top-8 left-4 z-20">
<v-btn @click="goBack" variant="plain"
class="rounded-full text-white w-12 h-12 flex items-center justify-center">
<v-icon class="text-black bg-white rounded-full" size="36">mdi-close</v-icon>
</v-btn>
</div>
<!-- Left arrow collée à gauche -->
<div v-if="multipleImages" class="absolute left-0 top-1/2 transform -translate-y-1/2 z-20">
<v-btn @click="previousImage" variant="plain" class="rounded-full bg-gray-800 text-white w-12 h-12">
<v-icon size="36">mdi-chevron-left</v-icon>
</v-btn>
</div>
<div class="flex items-center justify-center w-full h-full z-10">
<img :src="currentImage" alt="Image" class="max-w-full max-h-full object-contain" />
</div>
<!-- right arrow -->
<div v-if="multipleImages" class="absolute right-4 top-1/2 transform -translate-y-1/2 z-10">
<v-btn @click="nextImage" variant="plain" class="rounded-full bg-gray-800 text-white w-12 h-12">
<v-icon size="36">mdi-chevron-right</v-icon>
</v-btn>
</div>
</div>
<!-- Info -->
<div class="fixed-width border-l-2 p-6 bg-white overflow-y-auto max-h-screen">
<div class="border-b-2 p-6 font-sans space-y-2">
<div v-if="data && data.uri">
Uri: {{ data?.uri }}
<div v-if="data && data.createdByName">
Créé par: {{ data.createdByName }}
</div>
<div v-if="data && data.title" class="font-semibold">
Title: {{ data?.title }}
Titre: {{ data.title }}
</div>
<div v-if="data && data.description">
Description: {{ data?.description }}
</div>
<div v-if="data && data.createdAt">
Date: {{ data?.createdAt }}
</div>
<div v-if="data && data.createdBy">
Creator: {{ data?.createdBy }}
</div>
Description: {{ data.description }}
</div>
<div class="border-b-2 p-6">
<PostMessage :content-id="contentId">
</PostMessage>
<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>
<MessageList :content-id="contentId">
</MessageList>
<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>

View 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>

View File

@@ -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}`)
}
}

View 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>

View 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>

View File

@@ -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
}
]

View 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>

View File

@@ -1,182 +1,173 @@
<template>
<!-- Bannière-->
<div class="relative bottom-4 z-20 m">
<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 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="grid grid-cols-6 md:flex flex-wrap space-x-0 md:space-x-10 gap-6 ">
<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 GetActiveSocialNetworkUrls()"
:href="socialNetwork.url" target="_blank"
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">
<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>
<!--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>
</div>
</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.profileColors.bannerBottom || '#A30E79' }">
<div class="flex flex-col sm:flex-row items-center sm:justify-between">
<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">
<!-- Profile Image -->
<div class="absolute lg:ml-72 transform -translate-y-1/2 lg:-translate-y-1/2 z-20">
<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'"
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.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>
:style="{ borderColor: creator.colors.accent || '#A30E79', height: '150px'}"
/>
</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>
<!-- 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>
<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>
<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>
</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},
});
const userStore = useUserStore();
const emits = defineEmits(['content-posted'])
const isDialogActive = ref(false);
const message = ref('');
const files = ref([]);
const isArticle = ref(false);
const publishPost = () => {
// Logic to publish post
console.log('Post published:', message.value, files.value);
isDialogActive.value = false;
resetPostForm();
};
const cancelPost = () => {
isDialogActive.value = false;
resetPostForm();
};
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 (userSocialNetworks.facebookUrl !== ''){
socialNetworks.push({icon: "mdi-twitter", url: props.creator.socialNetworks.xUrl})
}
if (userSocialNetworks.instagramUrl !== ''){
socialNetworks.push({icon: "mdi-instagram", url: props.creator.socialNetworks.instagramUrl})
}
if (userSocialNetworks.tiktokUrl !== ''){
socialNetworks.push({icon: "/images/hutopymedia/icons/white/tiktokwhite.png", url: props.creator.socialNetworks.tikTokUrl})
}
if (userSocialNetworks.youtubeUrl !== ''){
socialNetworks.push({icon: "mdi-youtube", url: props.creator.socialNetworks.youtubeUrl})
}
if (userSocialNetworks.yourWebsiteUrl !== ''){
const websiteIconWithBackup = props.creator.storedDataUrls.websiteIconUrl === '' ? "mdi-web" : props.creator.storedDataUrls.websiteIconUrl;
socialNetworks.push({icon: websiteIconWithBackup, url: props.creator.socialNetworks.yourWebsiteUrl})
function addContent(content) {
emits('content-posted', content)
}
return socialNetworks;
function GetSocialsUrls() {
const socials = [];
if (props.creator.socials.facebookUrl !== null) {
socials.push({
icon: "mdi-facebook",
url: props.creator.socials.facebookUrl
})
}
if (props.creator.socials.instagramUrl !== null) {
socials.push({
icon: "mdi-instagram",
url: props.creator.socials.instagramUrl
})
}
if (props.creator.socials.xUrl !== null) {
socials.push({
icon: "mdi-twitter",
url: props.creator.socials.xUrl
})
}
if (props.creator.socials.linkedInUrl !== null) {
socials.push({
icon: 'mdi-linkedin',
url: props.creator.socials.linkedInUrl
})
}
if (props.creator.socials.tikTokUrl !== null) {
socials.push({
icon: '/images/socials/tiktok-white.png',
url: props.creator.socials.tikTokUrl
})
}
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 socials;
}
</script>

View File

@@ -8,7 +8,6 @@
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
creator: {

View File

@@ -28,6 +28,5 @@
<script setup>
import CreatorCard from "@/views/creators/CreatorCard.vue";
import creators from "@/views/creators/creators.json";
</script>

View File

@@ -1,45 +1,17 @@
<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 class="max-w-[1250px] mx-auto">
<creator-banner :creator="creator"
@content-posted="contentPosted"
></creator-banner>
</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-[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>

View 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>

View 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>

View 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>

View File

@@ -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"
}
]

View File

@@ -190,7 +190,3 @@
</div>
</template>
<style>
@import 'src/cssstyle/documentation.css';
</style>

View 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>

View 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>

View 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"
}
]
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<template>
<div class="fixed z-50 bottom-10 right-10 flex flex-column">
<div class="fixed z-50 bottom-6 right-6 flex flex-column">
<div
v-if="showPopup"
ref="popup"
@@ -27,7 +27,7 @@
</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);

View File

@@ -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,33 +73,42 @@
<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 }}
<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'">
<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-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>
@@ -92,41 +121,31 @@
</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);
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>

View File

@@ -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>

View File

@@ -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("");

View 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>

View File

@@ -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>

View File

@@ -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,19 +85,18 @@
</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 props = defineProps({
user: { type: MyUserModel },
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);
@@ -131,7 +130,7 @@ import {ref, defineProps, onMounted} from 'vue';
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
profilePictureUrl.value = e.target.result;
logoImageUrl.value = e.target.result;
};
reader.readAsDataURL(file);
emit('updateProfilePicture', file);
@@ -146,13 +145,13 @@ import {ref, defineProps, onMounted} from 'vue';
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;
};

View 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>

View 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>

View File

@@ -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>

View File

@@ -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 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>
</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-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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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, jai trouvé mon X avec les médias sociaux → Mon but est daider 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, jai 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, jai trouvé mon X avec les médias sociaux → Mon but est daider 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, jai 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
Rubiks Cube ça a lair si facile pour eux quon 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 dune 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 limpression dêtre un vrai casse-tête!
La clé cest 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 pour devenir
autonome à
les gérer par toi-même Je suis pour ça!
<br><br>
Je te promets quon 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>

View File

@@ -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 dune histoire, dune passion, dune 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">Cest 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", 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">Jai 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 !
Jy ai croisé Carl Vaillancourt, copropriétaire de lentreprise 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>

View File

@@ -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 dencourager la persévérance scolaire. Nous soulageons également les conditions des personnes atteintes d'un handicap en offrant des programmes récréatifs et dautres services de soutien visant à appuyer leur mieux-être physique, mental et émotif afin quils deviennent et demeurent des acteurs importants au sein de la collectivité. La Fondation Leffet 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">
Cest 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 lon nomme Leffet Papillon. Cette
théorie
implique que le battement dailes dun papillon au Brésil peut provoquer une tempête au Texas.
Selon lexpression, 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 samplifie progressivement et provoque, à long terme,
des
changements colossaux. Cette notion ne concerne plus seulement la météo, mais sapplique également aux
sciences humaines et à lenvironnement. <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 quil
soit
toujours positif.
<br><br>
Donc, voici toute lessence 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 lon nomme Leffet Papillon. Cette théorie implique que le battement
dailes dun papillon au Brésil peut provoquer une tempête au Texas.<br><br>
Selon lexpression, 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 samplifie progressivement et provoque, à long
terme,
des changements colossaux. Cette notion ne concerne plus seulement la météo, mais sapplique également
aux
sciences humaines et à lenvironnement.<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 quil
soit toujours positif.
Donc, voici toute lessence 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>

View File

@@ -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 lobjectif de démystifier le trouble de lautisme 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. Cest sur les médias sociaux, dont Facebook et YouTube, quil diffuse ces communications et rencontres inusitées. Son engagement et le dépassement de soi dans ses réalisations lui permettent de contribuer à élargir lacceptation 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 à Jachète Québécois, Jachè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>

View File

@@ -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"
<div class="flex flex-column py-2">
<div class="flex flex-row full">
<div class="w-full">
<div class="flex justify-between items-center">
<div>
<div>
<div class="content-center flex flex-row">
<img
:src="message.createdByPortraitUrl ?? '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'"
alt="Profile Image"
class="rounded-full"
width="32px"
height="32px">
</div>
height="32px"
/>
<span class="font-semibold font-sans mr-2 capitalize ml-2">
{{ message.createdByName }}
</span>
<div class="p-2 flex-1">
<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>
<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>
<v-menu :location="location">
</div>
</div>
<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: {
<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
}
},
methods: {
editMessage(message) {
// Logic for editing the message
console.log('Edit message', message);
},
deleteMessage(message) {
// Logic for deleting the message
});
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);
},
reportMessage(message) {
// Logic for reporting the 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>

View File

@@ -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>

View File

@@ -1,61 +1,134 @@
<template>
<div class="flex flex-column">
<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">
</div>
<v-text-field
v-model="message"
<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="solo-inverted"
variant="underlined"
placeholder="Votre commentaire..."
hide-details
clearable>
</v-text-field>
auto-grow
rows="1"
maxlength="1024"
class="pr-1 ml-6 flex-grow"
@keydown.enter.prevent="publish"
>
</v-textarea>
<div class="flex flex-col justify-center">
<v-btn
icon
variant="text"
@click="publish"
>
<v-icon>mdi-send</v-icon>
</v-btn>
</div>
</div>
</div>
</div>
<div class="flex justify-end items-center mt-1">
<div v-if="value.length < 1024" class="text-gray-500 text-sm">{{ value.length }}/1024</div>
<div v-if="value.length >= 1024" class="text-red-500 text-sm">{{ value.length }}/1024</div>
</div>
</div>
<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>
<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>

View File

@@ -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
}
]

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show More