Many fix and improvements

This commit is contained in:
Jonathan Bourdon
2024-08-03 04:15:55 -04:00
parent 0d94d79c77
commit 78ead7e387
37 changed files with 669 additions and 735 deletions

89
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@stripe/stripe-js": "^3.0.10", "@stripe/stripe-js": "^3.0.10",
"@vueuse/core": "^10.11.0",
"@xtiannyeto/vue-auth-social": "^0.1.9", "@xtiannyeto/vue-auth-social": "^0.1.9",
"axios": "^1.6.7", "axios": "^1.6.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@@ -889,6 +890,11 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA=="
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"node_modules/@ungap/structured-clone": { "node_modules/@ungap/structured-clone": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -1012,6 +1018,89 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.33.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.33.tgz",
"integrity": "sha512-aoRY0jQk3A/cuvdkodTrM4NMfxco8n55eG4H7ML/CRy7OryHfiqvug4xrCBBMbbN+dvXAetDDwZW9DXWWjBntA==" "integrity": "sha512-aoRY0jQk3A/cuvdkodTrM4NMfxco8n55eG4H7ML/CRy7OryHfiqvug4xrCBBMbbN+dvXAetDDwZW9DXWWjBntA=="
}, },
"node_modules/@vueuse/core": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz",
"integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.0",
"@vueuse/shared": "10.11.0",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz",
"integrity": "sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.0.tgz",
"integrity": "sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@xtiannyeto/vue-auth-social": { "node_modules/@xtiannyeto/vue-auth-social": {
"version": "0.1.9", "version": "0.1.9",
"resolved": "https://registry.npmjs.org/@xtiannyeto/vue-auth-social/-/vue-auth-social-0.1.9.tgz", "resolved": "https://registry.npmjs.org/@xtiannyeto/vue-auth-social/-/vue-auth-social-0.1.9.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@stripe/stripe-js": "^3.0.10", "@stripe/stripe-js": "^3.0.10",
"@vueuse/core": "^10.11.0",
"@xtiannyeto/vue-auth-social": "^0.1.9", "@xtiannyeto/vue-auth-social": "^0.1.9",
"axios": "^1.6.7", "axios": "^1.6.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",

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> <template>
<v-app v-if="isUserLoaded"> <v-app>
<div class="m-0 flex flex-column h-screen"> <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 class="flex flex-row relative">
<div <div
@mouseenter="openSidebar" @mouseenter="openSidebar"
@@ -10,7 +10,7 @@
></div> ></div>
<transition name="slide-fade"> <transition name="slide-fade">
<div v-show="!hideSideBar" <div v-show="sideBarStore.visible"
@mouseleave="startCloseSidebarTimer" @mouseleave="startCloseSidebarTimer"
@mouseenter="clearCloseSidebarTimer" @mouseenter="clearCloseSidebarTimer"
class=" fixed h-full min-w-60 border-r-2 bg-white z-30 transition-transform duration-700"> class=" fixed h-full min-w-60 border-r-2 bg-white z-30 transition-transform duration-700">
@@ -31,38 +31,24 @@
import Header from "@/views/main/Header.vue"; import Header from "@/views/main/Header.vue";
import Footer from "@/views/main/Footer.vue"; import Footer from "@/views/main/Footer.vue";
import SideBar from "@/views/main/SideBar.vue"; import SideBar from "@/views/main/SideBar.vue";
import {ref, onMounted, onUnmounted, onBeforeMount} from 'vue'; import {ref, onMounted, onUnmounted} from 'vue';
import {eventBus} from '@/eventBus.js'; import {useSideBarStore} from "@/stores/sideBarStore.js";
import {useUserStore} from "@/stores/user.js";
import {useClient} from "@/plugins/api.js";
const hideSideBar = ref(true);
const isUserLoaded = ref(false);
const showPopup = ref(false); const showPopup = ref(false);
const popup = ref(null); const popup = ref(null);
const popupButton = ref(null); const popupButton = ref(null);
let closeSidebarTimer = null; let closeSidebarTimer = null;
let client = useClient(); const sideBarStore = useSideBarStore()
let userStore = useUserStore();
onBeforeMount(async () => {
await userStore.setCurrentUser(client);
isUserLoaded.value = true;
});
const toggleSidebar = () => {
hideSideBar.value = !hideSideBar.value;
};
const openSidebar = () => { const openSidebar = () => {
clearCloseSidebarTimer(); clearCloseSidebarTimer();
hideSideBar.value = false; sideBarStore.show()
}; };
const startCloseSidebarTimer = () => { const startCloseSidebarTimer = () => {
closeSidebarTimer = setTimeout(() => { closeSidebarTimer = setTimeout(() => {
hideSideBar.value = true; sideBarStore.hide()
}, 500); }, 500);
}; };
@@ -83,18 +69,16 @@ const handleClickOutside = (event) => {
!event.target.closest('.w-48') && !event.target.closest('.w-48') &&
!event.target.closest('.v-app-bar-nav-icon') !event.target.closest('.v-app-bar-nav-icon')
) { ) {
hideSideBar.value = true; sideBarStore.hide()
} }
}; };
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
eventBus.value.toggleSidebar = toggleSidebar;
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside);
eventBus.value.toggleSidebar = null;
}); });
</script> </script>

View File

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

View File

@@ -8,7 +8,6 @@ import 'vuetify/styles'
import {createVuetify} from 'vuetify' import {createVuetify} from 'vuetify'
import * as components from 'vuetify/components' import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives' import * as directives from 'vuetify/directives'
import clientPlugin from './plugins/api.js'
import vueGoogleOauth from 'vue3-google-login' import vueGoogleOauth from 'vue3-google-login'
const vuetify = createVuetify({ const vuetify = createVuetify({
@@ -17,7 +16,6 @@ const vuetify = createVuetify({
}); });
createApp(App) createApp(App)
.use(clientPlugin)
.use(createPinia()) .use(createPinia())
.use(vuetify) .use(vuetify)
.use(router) .use(router)

View File

@@ -1,48 +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 = "";
creatorAlias = "";
alias = null;
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 = null;
profilePictureUrl = null;
websiteIconUrl = null;
static createFromApiResult(apiResult){
return Object.assign(new StoredDataUrlsModel(), apiResult)
}
}

View File

@@ -1,9 +1,7 @@
import axios from "axios"; import axios from "axios"
import {inject} from "vue"; import {useAuthStore} from "@/stores/authStore.js"
const key = Symbol("api"); export function useClient() {
export default function(app) {
if (!import.meta.env.VITE_API_URL) throw new Error("VITE_API_URL is not provided") 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 // You create a .env.development file and a .env file
@@ -13,23 +11,16 @@ export default function(app) {
timeout: 10000, timeout: 10000,
}); });
const authStore = useAuthStore()
const requestInterceptor = (config) => { const requestInterceptor = (config) => {
const token = localStorage.getItem("jwt"); if (authStore.isAuthenticated) {
if (token) config.headers["Authorization"] = `Bearer ${token}`; config.headers["Authorization"] = `Bearer ${authStore.accessToken}`
return config; }
return config
} }
api.interceptors.request.use(requestInterceptor); 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; return api;
} }

View File

@@ -1,5 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from "@/stores/user.js";
import GuillaumeAime from '@/views/manualusers/GuillaumeAime.vue' import GuillaumeAime from '@/views/manualusers/GuillaumeAime.vue'
import About from '@/views/documentation/About.vue' import About from '@/views/documentation/About.vue'
import ContentPolicy from '@/views/documentation/ContentPolicy.vue' import ContentPolicy from '@/views/documentation/ContentPolicy.vue'
@@ -22,6 +21,7 @@ import CreatorList from '../views/creators/CreatorList.vue'
import CreatorPage from "@/views/creators/CreatorPage.vue"; import CreatorPage from "@/views/creators/CreatorPage.vue";
import ContentPage from "@/views/contents/ContentPage.vue"; import ContentPage from "@/views/contents/ContentPage.vue";
import PostContent from "@/views/contents/PostContent.vue"; import PostContent from "@/views/contents/PostContent.vue";
import {useAuthStore} from "@/stores/authStore.js";
const routes = [ const routes = [
{ {
@@ -114,18 +114,6 @@ const routes = [
component: Join, component: Join,
meta: { hideSideBar: true } meta: { hideSideBar: true }
}, },
{
path: '/paymentcompleted',
name: 'PayementCompleted',
component: PaymentCompleted
},
{
path: '/profile',
name: 'profile',
component: Profile,
meta: { requiresAuth: true }
},
{ {
path: '/signup', path: '/signup',
name: 'signup', name: 'signup',
@@ -136,12 +124,23 @@ const routes = [
name: 'login', name: 'login',
component: LoginView component: LoginView
}, },
{
path: '/paymentcompleted',
name: 'PayementCompleted',
component: PaymentCompleted,
},
{ {
path: '/wallet', path: '/wallet',
name: 'wallet', name: 'wallet',
component: Wallet, component: Wallet,
meta: { requiresAuth: true } meta: { requiresAuth: true }
} },
{
path: '/profile',
name: 'profile',
component: Profile,
meta: { requiresAuth: true }
},
] ]
const router = createRouter({ const router = createRouter({
@@ -151,11 +150,10 @@ const router = createRouter({
// Navigation gards // Navigation gards
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const authStore = useUserStore(); const authStore = useAuthStore();
if (to.matched.some(record => record.meta.requiresAuth)) { if (to.matched.some(record => record.meta.requiresAuth)) {
if (!authStore.user.value || !Object.keys(authStore.user.value).length) { if (!authStore.isAuthenticated) {
next('/'); next('/');
} else { } else {
next(); 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);
}
}
});

84
src/stores/authStore.js Normal file
View File

@@ -0,0 +1,84 @@
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";
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)
function updateTokens(data) {
accessToken.value = data.accessToken
refreshToken.value = data.refreshToken
}
function cleanTokens() {
updateTokens({
accessToken: undefined,
refreshToken: undefined,
})
}
function logout() {
cleanTokens()
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, login, loginGoogle, logout}
})

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

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

77
src/stores/userStore.js Normal file
View File

@@ -0,0 +1,77 @@
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, oldValue) => {
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 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;
}
}
async function updateCurrentUser(userModel, profilePicture) {
const client = useClient()
await client.patch("/api/UpdateMyUser/profile", userModel)
if (typeof userModel.storedDataUrls.profilePictureUrl !== "object") {
const haveNewProfilePicture = profilePicture !== null && profilePicture.size !== 0;
const updateProfilePictureEndpoint = haveNewProfilePicture ? `/api/UpdateMyUser/profile-picture` : `/api/UpdateMyUser/profile-picture?url=${userModel.storedDataUrls.profilePictureUrl}`;
const response = await client.post(updateProfilePictureEndpoint, profilePicture, {
headers: {
'Content-Type': profilePicture?.type ?? "application/octet-stream",
}
});
if (haveNewProfilePicture) {
this.user.value.portraitUrl = response.data;
}
}
this.user.value = userModel;
}
return {user, creator, alias, portraitUrl}
})

View File

@@ -2,107 +2,107 @@
<div class="hidden sm:block" style="height: 40px"></div> <div class="hidden sm:block" style="height: 40px"></div>
<div> <div>
<div class="flex flex-col lg:flex-row items-center justify-center"> <div class="flex flex-col lg:flex-row items-center justify-center">
<div class="max-w-[700px] min-w-[300px]"> <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"> <img class="rounded-none sm:rounded-2xl sm:w-full mr-8" src="/images/hutopymedia/loginpage/loginhutopy.png"
</div> alt="hutopy login">
</div>
<div class="flex flex-col items-center min-w-[300px] m-12"> <div class="flex flex-col items-center min-w-[300px] m-12">
<h1 class="text-center text-2xl font-bold mb-5">Connexion</h1> <h1 class="text-center text-2xl font-bold mb-5">Connexion</h1>
<google-login class="w-full" :callback="googleCallback" popup-type="TOKEN"> <google-login class="w-full" :callback="googleCallback" popup-type="TOKEN">
<template #default> <template #default>
<v-btn density="comfortable" class="mb-2 w-full"> <v-btn density="comfortable" class="mb-2 w-full">
<v-icon left>mdi-google</v-icon> <v-icon left>mdi-google</v-icon>
Google 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="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> </v-btn>
</template>
</google-login>
<!-- <v-btn density="comfortable" class="mb-2 w-full">--> <p class="mt-4 text-sm text-center">
<!-- <v-icon left>mdi-facebook</v-icon>--> Si vous n'avez pas de compte, <a href="/register" class="text-blue-500">cliquez ici</a> pour en créer un.
<!-- Facebook--> </p>
<!-- </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"> <div v-if="errorSnackBar" class="mb-4 text-red-600">
<v-icon left>mdi-account</v-icon> Nom d'utilisateur ou mot de passe invalide.
Utilisateur <button class="text-red-600 ml-4" @click="errorSnackBar = false">Fermer</button>
</v-btn> </div>
<div v-if="showEmailForm" class="w-full mt-2">
<v-text-field v-model="user.email"
label="Courriel"
variant="outlined"
dense
prepend-inner-icon="mdi-email"
color="transparent"
class="text-black"
></v-text-field>
<v-text-field v-model="user.password"
label="Mot de passe"
:type="showPassword ? 'text' : 'password'"
variant="outlined"
dense
prepend-inner-icon="mdi-lock"
append-inner-icon="mdi-eye"
@click:append-inner="showPassword = !showPassword"
color="transparent"
class="text-black"
></v-text-field>
<v-btn class="w-full text-center text-white" :style="{ backgroundColor: '#A30E79' }" @click="login">Connecter</v-btn>
<p class="mt-4 text-sm text-center">
Si vous n'avez pas de compte, <a href="/register" class="text-blue-500">cliquez ici</a> pour en créer un.
</p>
<div v-if="errorSnackBar" class="mb-4 text-red-600">
Nom d'utilisateur ou mot de passe invalide.
<button class="text-red-600 ml-4" @click="errorSnackBar = false">Fermer</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<selected-footer></selected-footer> <selected-footer></selected-footer>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import {ref} from 'vue';
import { useRouter } from 'vue-router'; import {useRouter} from 'vue-router';
import { useClient } from "@/plugins/api.js"; import {useAuthStore} from '@/stores/authStore.js';
import { auth } from '@/stores/auth.js'; import {GoogleLogin} from "vue3-google-login";
import { GoogleLogin } from "vue3-google-login";
// import { FacebookAuth } from '@xtiannyeto/vue-auth-social'; // import { FacebookAuth } from '@xtiannyeto/vue-auth-social';
import SelectedFooter from "@/views/main/SelectedFooter.vue"; import SelectedFooter from "@/views/main/SelectedFooter.vue";
const api = useClient(); const authStore = useAuthStore();
const store = auth();
const router = useRouter(); const router = useRouter();
const user = ref({}); const email = ref("");
const password = ref("");
const errorSnackBar = ref(false); const errorSnackBar = ref(false);
const showEmailForm = ref(false); const showEmailForm = ref(false);
const showPassword = ref(false); const showPassword = ref(false);
async function login() { async function login() {
try { const result = await authStore.login(email.value, password.value);
await store.login(api, user.value.email, user.value.password); if (result === true) {
await router.push('/'); await router.push('/')
window.location.reload();
} catch (error) {
errorSnackBar.value = true;
} }
} }
const googleCallback = async (response) => { const googleCallback = async (response) => {
await store.loginGoogle(api, response["access_token"]); await authStore.loginGoogle(response["access_token"]);
await router.push("/"); await router.push("/");
}; };
@@ -110,4 +110,5 @@ const googleCallback = async (response) => {
// const facebookCallback = (response) => { // const facebookCallback = (response) => {
// console.log("User Successfully Logged In", response); // console.log("User Successfully Logged In", response);
// }; // };
</script> </script>

View File

@@ -106,7 +106,7 @@
<button <button
@click="openModal('ModalTikTok')" @click="openModal('ModalTikTok')"
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 "> 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/black/tiktokblack.png" class="w-5 h-5" ></span> <span class="flex-none pa-2 min-w-32 text-left"> <img src="/images/externals/tiktok-black.png" class="w-5 h-5" ></span>
<span class="flex-auto text-left pr-6">TikTok</span> <span class="flex-auto text-left pr-6">TikTok</span>
<span class="flex-none"> <span class="flex-none">
<v-icon>mdi-chevron-right</v-icon> <v-icon>mdi-chevron-right</v-icon>

View File

@@ -1,45 +1,86 @@
<template> <script setup>
import {computed, reactive, onMounted} 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 YoutubePlayer from './YoutubePlayer.vue';
import ImageViewer from './ImageViewer.vue';
// import VimeoPlayer from '@/components/VimeoPlayer.vue';
// import AudioPlayer from '@/components/AudioPlayer.vue';
// import BlogViewer from '@/components/BlogViewer.vue';
const props = defineProps({
content: {
type: Object,
required: true,
}
});
const hasUrls = computed(() => !!props.content.urls && props.content.urls.length > 0);
const messages = reactive([]);
function addMessage(newMessage) {
messages.unshift(newMessage)
}
function getComponent(url) {
if (url.includes('youtube.com') || url.includes('youtu.be')) {
return YoutubePlayer;
// } else if (url.includes('vimeo.com')) {
// return VimeoPlayer;
} else if (url.match(/\.(jpeg|jpg|gif|png)$/)) {
return ImageViewer;
// } else if (url.match(/\.(mp3|wav|ogg)$/)) {
// return 'AudioPlayer';
// } else {
// return 'BlogViewer';
}
}
</script>
<template>
<div class="shadow-md rounded-lg bg-gray-50"> <div class="shadow-md rounded-lg bg-gray-50">
<div class="text-lg font-bold"> <v-card
{{ props.content.title }} outlined
<span class="text-md-caption"> tile
{{ time_ago(props.content.createdAt) }} >
<v-card-title>
{{ props.content.title }}
<span class="text-subtitle-2">
{{ time_ago(props.content.createdAt) }}
</span> </span>
</div> </v-card-title>
<div v-if="props.content.url !== null || props.content.uri !== null" <v-carousel v-if="hasUrls">
class="h-48 object-cover bg-gray-300 rounded-md">
<v-img :src="props.content.url" <v-carousel-item
v-if="!isHttpUrl"> v-for="url in props.content.urls"
</v-img> :key="url"
>
<component :is="getComponent(url)"
:src="url"
></component>
</v-carousel-item>
<iframe v-if="isHttpUrl" </v-carousel>
: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>
</div> <v-card-text>
{{ props.content.description }}
</v-card-text>
<v-card-actions>
<div class="flex flex-row"> <router-link :to="'content/' + content.id">
<div>
<div class="text-sm text-gray-500">{{ props.content.description }}</div>
</div>
<div>
<router-link :to="'content/' + props?.content?.id">
<div class="bg-blue-500 rounded-lg py-1 px-2">Plus ...</div> <div class="bg-blue-500 rounded-lg py-1 px-2">Plus ...</div>
</router-link> </router-link>
</div> </v-card-actions>
</div> </v-card>
<div> <div>
@@ -58,28 +99,4 @@
</div> </div>
</template> </template>
<script setup>
import {computed, reactive} from 'vue';
import {time_ago} from "@/internal_time_ago.js";
import MessageList from "@/views/messages/MessageList.vue";
import PostMessage from "@/views/messages/PostMessage.vue";
const isHttpUrl = computed(() => props.content?.uri?.startsWith('http'))
const props = defineProps({
content: {
type: Object,
required: true,
}
});
const messages = reactive([]);
function addMessage(newMessage) {
messages.unshift(newMessage)
}
</script>

View File

@@ -48,7 +48,6 @@ async function load({done}) {
let uri = `/api/contents/user/${props.creatorId}?page_size=${page_size}` let uri = `/api/contents/user/${props.creatorId}?page_size=${page_size}`
if (last_id !== null) uri = uri + `&last_id=${last_id}` if (last_id !== null) uri = uri + `&last_id=${last_id}`
console.log(`Fetching content at: ${uri}`)
const response = await client.get(uri) const response = await client.get(uri)
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {

View File

@@ -1,72 +1,8 @@
<template> <script setup>
<div v-if="creator.id === userStore.getCurrentUser().id">
<button
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-card class="text-center rounded-xl">
<v-card-title class="text-white p-4 rounded-t"
:style="{backgroundColor: creator.profileColors.menu || '#A30E79'}">
Quicky
</v-card-title>
<v-card-text>
<v-text-field v-model="title"
class="p-2"
label="Titre"
density="comfortable"
variant="outlined"
hide-details
clearable>
</v-text-field>
<v-textarea v-model="message"
label="Écrivez votre message ici..."
class="p-2"
density="comfortable"
variant="outlined"
hide-details
clearable
outlined>
</v-textarea>
<v-file-input v-model="files"
label="Ajoutez des fichiers"
class="p-2"
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="secondary" @click="cancelPost">Annuler</v-btn>
<v-btn color="primary" @click="publishPost">Publier</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup>
import {useClient} from '@/plugins/api.js'; import {useClient} from '@/plugins/api.js';
import {ref} from 'vue'; import {ref} from 'vue';
import {useUserStore} from '@/stores/user.js'; import {useUserStore} from '@/stores/userStore.js';
import {v7} from 'uuid'
const props = defineProps({ const props = defineProps({
creator: {type: Object, required: true}, creator: {type: Object, required: true},
@@ -75,22 +11,34 @@ const props = defineProps({
const userStore = useUserStore(); const userStore = useUserStore();
const isDialogActive = ref(false); const isDialogActive = ref(false);
const title = ref('');
const message = ref('');
const files = ref([]);
const client = useClient(); const client = useClient();
const title = ref('');
const message = ref('');
const files = ref([])
const publishPost = async () => { 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 { try {
await client.post( const response = await client.post(
`/api/contents/`, `/api/contents/`,
formData,
{ {
"title": title.value, headers: {
"description": message.value 'Content-Type': 'multipart/form-data',
}
}) })
console.log('Files uploaded successfully:', response.data);
closeDialog() closeDialog()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -110,3 +58,69 @@ const closeDialog = () => {
</script> </script>
<template>
<button
v-if="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">
<v-card-title class="text-white p-4 rounded-t"
:style="{backgroundColor: creator.profileColors.menu || '#A30E79'}">
Quicky
</v-card-title>
<v-card-text>
<v-text-field v-model="title"
class="p-2"
label="Titre"
density="comfortable"
variant="outlined"
hide-details
clearable
></v-text-field>
<v-textarea v-model="message"
label="Écrivez votre message ici..."
class="p-2"
density="comfortable"
variant="outlined"
hide-details
clearable
outlined
></v-textarea>
<v-file-input v-model="files"
label="Ajoutez des fichiers"
class="p-2"
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="secondary" @click="cancelPost">Annuler</v-btn>
<v-btn color="primary" @click="publishPost">Publier</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-dialog>
</template>

View File

@@ -0,0 +1,31 @@
<template>
<div class="image-container">
<img :src="src" alt="Image" class="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

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

@@ -86,10 +86,9 @@
</template> </template>
<script setup> <script setup>
import MyUserModel from "@/models/myUserModel.js";
const props = defineProps({ const props = defineProps({
user: {type: MyUserModel}, user
}); });
const dateRule = value => { const dateRule = value => {

View File

@@ -15,7 +15,8 @@
class="text-white text-2xl transform transition-transform duration-200 hover:scale-125 hover:text-blue-500"> 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> <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('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('websiteIcon')" :src="socialNetwork.icon" class="w-9 h-9"
alt="Website">
</a> </a>
</div> </div>
</div> </div>
@@ -91,29 +92,29 @@ const props = defineProps({
function GetActiveSocialNetworkUrls() { function GetActiveSocialNetworkUrls() {
const socialNetworks = []; const socialNetworks = [];
const userSocialNetworks = props.creator.socialNetworks; const userSocialNetworks = props.creator.socialNetworks;
if (userSocialNetworks.facebookUrl !== '') { if (userSocialNetworks.facebookUrl !== null) {
socialNetworks.push({icon: "mdi-facebook", url: props.creator.socialNetworks.facebookUrl}) socialNetworks.push({icon: "mdi-facebook", url: props.creator.socialNetworks.facebookUrl})
} }
if (userSocialNetworks.facebookUrl !== '') { if (userSocialNetworks.xUrl !== null) {
socialNetworks.push({icon: "mdi-twitter", url: props.creator.socialNetworks.xUrl}) socialNetworks.push({icon: "mdi-twitter", url: props.creator.socialNetworks.xUrl})
} }
if (userSocialNetworks.instagramUrl !== '') { if (userSocialNetworks.instagramUrl !== null) {
socialNetworks.push({icon: "mdi-instagram", url: props.creator.socialNetworks.instagramUrl}) socialNetworks.push({icon: "mdi-instagram", url: props.creator.socialNetworks.instagramUrl})
} }
if (userSocialNetworks.tiktokUrl !== '') { if (userSocialNetworks.tikTokUrl !== null) {
socialNetworks.push({ socialNetworks.push({
icon: "/images/hutopymedia/icons/white/tiktokwhite.png", icon: '/images/externals/tiktok-white.png',
url: props.creator.socialNetworks.tikTokUrl url: props.creator.socialNetworks.tikTokUrl
}) })
} }
if (userSocialNetworks.youtubeUrl !== '') { if (userSocialNetworks.youtubeUrl !== null) {
socialNetworks.push({icon: "mdi-youtube", url: props.creator.socialNetworks.youtubeUrl}) socialNetworks.push({icon: 'mdi-youtube', url: props.creator.socialNetworks.youtubeUrl})
} }
if (userSocialNetworks.yourWebsiteUrl !== ''){ if (userSocialNetworks.websiteUrl !== null) {
const websiteIconWithBackup = props.creator.storedDataUrls.websiteIconUrl === '' ? "mdi-web" : props.creator.storedDataUrls.websiteIconUrl; socialNetworks.push({icon: 'mdi-web', url: props.creator.socialNetworks.websiteUrl})
socialNetworks.push({icon: websiteIconWithBackup, url: props.creator.socialNetworks.yourWebsiteUrl})
} }
return socialNetworks; return socialNetworks;

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

@@ -4,7 +4,7 @@
@click.stop @click.stop
> >
<div class="flex items-center"> <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> </v-app-bar-nav-icon>
<RouterLink to="/" class="hidden md:block"> <RouterLink to="/" class="hidden md:block">
@@ -53,51 +53,52 @@
<div class="text-center"> <div class="text-center">
<v-menu open-on-hover> <v-menu open-on-hover>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn variant="plain" v-bind="props" class="d-flex align-center text-capital-none"> <div v-bind="props" class="flex align-center font-sans py-1 px-4 rounded-lg hover:bg-gray-100">
<span class="normal-case max-w-xs hidden md:block"> <span class="max-w-xs hidden md:block">
{{ currentUser.userName }} {{ userStore.alias }}
</span> </span>
<img <img
:src="currentUser.storedDataUrls.profilePictureUrl ?? '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'" :src="userStore.portraitUrl"
alt="Profile Image" alt="Profile Image"
class="ml-2 rounded-full" style="width: 32px; height: 32px;"> class="ml-2 rounded-full"
</v-btn> width="32"
height="32">
</div>
</template> </template>
<v-list min-width="200px" class=" align-center mt-3 left-3"> <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 class="nav-button">
<v-list-item-title> <v-list-item-title>
<v-btn to="/login" class="w-100 " variant="plain">Connexion</v-btn> <v-btn to="/login" class="w-100 " variant="plain">Connexion</v-btn>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</div> </template>
<div v-if="currentUser.userName !== 'Anonyme'"> <template v-else>
<v-list-item v-if="userStore.creator" class="nav-button">
<v-list-item v-if="currentUser.creatorAlias !== null" class="nav-button"> <router-link :to="`/@${userStore.creator.name}`">
<router-link :to="`/@${currentUser.creatorAlias}`"> <v-btn class="w-100 " variant="plain">@{{ userStore.creator.name }}</v-btn>
<v-btn class="w-100 " variant="plain">@{{ currentUser.creatorAlias }}</v-btn>
</router-link> </router-link>
</v-list-item> </v-list-item>
<v-list-item class="nav-button"> <v-list-item class="nav-button">
<v-list-item-title> <v-list-item-title>
<v-btn to="/profile" class="w-100 " variant="plain">Mon profil</v-btn> <v-btn to="/profile" class="w-100" variant="plain">Mon profil</v-btn>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item class="nav-button"> <v-list-item class="nav-button">
<v-list-item-title> <v-list-item-title>
<v-btn to="/wallet" class="w-100 " variant="plain"> Portefeuille</v-btn> <v-btn to="/wallet" class="w-100" variant="plain">Portefeuille</v-btn>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item class="nav-button"> <v-list-item class="nav-button">
<v-list-item-title> <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-title>
</v-list-item> </v-list-item>
</div> </template>
</v-list> </v-list>
</v-menu> </v-menu>
</div> </div>
@@ -108,21 +109,19 @@
</template> </template>
<script setup> <script setup>
import {ref, onBeforeUnmount, onBeforeMount, watch, reactive} from "vue"; import {ref, onBeforeUnmount, onBeforeMount, watch} from "vue";
import {eventBus} from '@/eventBus.js';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import {useUserStore} from "@/stores/user.js"; import {useSideBarStore} from '@/stores/sideBarStore.js';
import MyUserModel from "@/models/myUserModel.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 router = useRouter();
const searchQuery = ref(""); const searchQuery = ref("");
const showSearch = ref(false); const showSearch = ref(false);
let currentUser = reactive(MyUserModel.getDefaultUser());
const userStore = useUserStore();
const toggleSidebar = () => {
eventBus.value.toggleSidebar();
};
const onSearch = () => { const onSearch = () => {
const query = searchQuery.value.trim(); const query = searchQuery.value.trim();
@@ -148,33 +147,14 @@ const handleClickOutside = (event) => {
} }
}; };
const navigateToMessages = () => {
router.push('/messages');
};
const logout = () => {
localStorage.removeItem('jwt');
window.location.reload();
};
onBeforeMount(() => { onBeforeMount(() => {
currentUser = userStore.getCurrentUser(); 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(() => { onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside);
}); });
</script> </script>

View File

@@ -68,9 +68,8 @@
</template> </template>
<script setup> <script setup>
import {ref, onBeforeMount, onMounted} from "vue"; import {ref, onMounted} from "vue";
import {useUserStore} from "@/stores/user.js"; import {useUserStore} from "@/stores/userStore.js";
import {useClient} from "@/plugins/api.js";
import SizeIndicator from "@/views/tools/SizeIndicator.vue"; import SizeIndicator from "@/views/tools/SizeIndicator.vue";
import ManageAccount from "@/views/Profile/ManageAccount.vue"; import ManageAccount from "@/views/Profile/ManageAccount.vue";
import PageInformations from "@/views/Profile/PageInformations.vue"; import PageInformations from "@/views/Profile/PageInformations.vue";
@@ -78,21 +77,12 @@ import PersonnalInfo from "@/views/Profile/PersonnalInfo.vue";
import AccountSecurity from "@/views/Profile/AccountSecurity.vue"; import AccountSecurity from "@/views/Profile/AccountSecurity.vue";
const userStore = useUserStore(); const userStore = useUserStore();
const client = useClient();
const currentUser = ref(null);
const currentComponent = ref('PageInformations'); // Default component const currentComponent = ref('PageInformations'); // Default component
const isDown = ref(false); const isDown = ref(false);
const startX = ref(0); const startX = ref(0);
const scrollLeft = ref(0); const scrollLeft = ref(0);
onBeforeMount(() => {
try {
currentUser.value = userStore.getCurrentUser();
} catch (error) {
console.log("User not logged")
}
});
onMounted(() => { onMounted(() => {
const slider = document.querySelector('.custom-scroll'); const slider = document.querySelector('.custom-scroll');
@@ -163,9 +153,6 @@ const scrollRightFunc = () => {
</script> </script>
<style scoped> <style scoped>
.save-btn {
z-index: 10;
}
.custom-scroll { .custom-scroll {
-ms-overflow-style: none; /* Internet Explorer 10+ */ -ms-overflow-style: none; /* Internet Explorer 10+ */
@@ -176,8 +163,4 @@ const scrollRightFunc = () => {
display: none; /* Safari and Chrome */ display: none; /* Safari and Chrome */
} }
.active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
</style> </style>

View File

@@ -86,14 +86,13 @@
<script setup> <script setup>
import {ref} from 'vue'; import {ref} from 'vue';
import MyUserModel from "@/models/myUserModel.js";
const emit = defineEmits(["updateProfilePicture", "updateBannerPicture"]); const emit = defineEmits(["updateProfilePicture", "updateBannerPicture"]);
const fallbackProfilePictureUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png'; const fallbackProfilePictureUrl = '/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png';
const fallbackBannerPictureUrl = '/images/usersmedia/HutopyProfile/banners/banner01.png'; const fallbackBannerPictureUrl = '/images/usersmedia/HutopyProfile/banners/banner01.png';
const props = defineProps({ const props = defineProps({
user: {type: MyUserModel}, user: {}
}); });
const bannerImageUrl = ref(props.user.storedDataUrls.bannerPictureUrl); const bannerImageUrl = ref(props.user.storedDataUrls.bannerPictureUrl);

View File

@@ -1,87 +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 {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> <script async setup>
import { onBeforeMount, ref, computed } from 'vue'; import { onBeforeMount, ref, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import {useUserStore} from "@/stores/user.js"; import {useUserStore} from "@/stores/userStore.js";
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
@@ -75,10 +75,8 @@ const transactionCount = computed(() => userTransactions.value.length);
onBeforeMount( () => { onBeforeMount( () => {
try { try {
const myUser = userStore.getCurrentUser(); userTransactions.value = userStore.user.userTransactions;
totalBalance.value = userStore.user.totalBalance;
userTransactions.value = myUser.userTransactions;
totalBalance.value = myUser.totalBalance;
} catch (error) { } catch (error) {
navigateToHome(); navigateToHome();
} }

View File

@@ -45,7 +45,7 @@
</a> </a>
<a href="https://www.tiktok.com/@guillaumeaime"> <a href="https://www.tiktok.com/@guillaumeaime">
<img class="socialicons invert-color" src="/images/hutopymedia/icons/white/tiktokwhite.png" <img class="socialicons invert-color" src="/images/externals/tiktok-white.png"
alt="Description image 2"> alt="Description image 2">
</a> </a>
</v-row> </v-row>

View File

@@ -2,7 +2,7 @@
<div class="flex flex-column"> <div class="flex flex-column">
<div class="flex flex-row p-1 items-center"> <div class="flex flex-row p-1 items-center">
<div class="px-2 content-center"> <div class="px-2 content-center">
<img :src="message.profileImageUrl ?? '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'" <img :src="message.createdByPortraitUrl ?? '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png'"
alt="Profile Image" alt="Profile Image"
class="rounded-full" class="rounded-full"
width="32px" width="32px"

View File

@@ -59,7 +59,6 @@ onBeforeMount(async () => {
await load({ await load({
page_size: 2, page_size: 2,
done: function (status) { done: function (status) {
console.log(`Loading status: ${status}`)
} }
}) })
}) })
@@ -71,8 +70,7 @@ async function load({done, page_size}) {
try { try {
let uri = `/api/messages/${props.subjectId}?page_size=${page_size}` let uri = `/api/messages/${props.subjectId}?page_size=${page_size}`
if (last_id !== null) uri = uri + `&last_id=${last_id}` if (last_id !== null) uri = uri + `&last_id=${last_id}`
console.log(`Fetching messages at: ${uri}`)
const response = await client.get(uri) const response = await client.get(uri)
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {

View File

@@ -6,7 +6,7 @@
<div class="mx-2 content-center"> <div class="mx-2 content-center">
<img :src="profileUrl" <img :src="userStore.portraitUrl"
alt="Profile Image" alt="Profile Image"
class="rounded-full" class="rounded-full"
width="32px" width="32px"
@@ -35,10 +35,12 @@
<script setup> <script setup>
import {useClient} from '@/plugins/api.js';
import {ref} from 'vue'; import {ref} from 'vue';
import {useUserStore} from "@/stores/user.js";
import {v7} from 'uuid' import {v7} from 'uuid'
import {useRouter} from "vue-router";
import {useClient} from '@/plugins/api.js';
import {useUserStore} from "@/stores/userStore.js";
import {useAuthStore} from "@/stores/authStore.js";
const props = defineProps({ const props = defineProps({
subjectId: { subjectId: {
@@ -51,13 +53,19 @@ const emit = defineEmits(['message-posted'])
const client = useClient() const client = useClient()
const value = ref("") const value = ref("")
const user = useUserStore() const router = useRouter()
const profileUrl = ref(user.getCurrentUser().storedDataUrls.profilePictureUrl ?? '/images/usersmedia/anonyme/profilepictures/profileAnonymeSquare.png') const userStore = useUserStore()
const authStore = useAuthStore()
const publish = async () => { const publish = async () => {
if (!authStore.isAuthenticated) {
await router.push('/login')
}
try { try {
const messageId = v7() const messageId = v7()
await client.post(`/api/messages/`, await client.post(`/api/messages/`,
{ {
"id": messageId, "id": messageId,
@@ -65,17 +73,16 @@ const publish = async () => {
"message": value.value "message": value.value
}) })
const currentUser = user.getCurrentUser()
emit('message-posted', emit('message-posted',
{ {
"id": messageId, "id": messageId,
"subjectId": props.subjectId, "subjectId": props.subjectId,
"createdBy": currentUser.id, "createdBy": userStore.user.id,
"createdByName": currentUser.alias ?? `${currentUser.firstName} ${currentUser.lastName}`, "createdByName": userStore.alias,
"createdByPortraitUrl": currentUser.storedDataUrls.profilePictureUrl, "createdByPortraitUrl": userStore.portraitUrl,
"createdAt": new Date(Date.now()).toISOString(), "createdAt": new Date(Date.now()).toISOString(),
"value": value.value, "value": value.value,
parentId: null, "parentId": null
}) })
value.value = '' value.value = ''