Merge remote-tracking branch 'origin/main' into I18N

# Conflicts:
#	src/views/main/Header.vue
This commit is contained in:
Dominic Villemure
2024-08-25 10:55:06 -04:00
10 changed files with 371 additions and 42 deletions

View File

@@ -2,10 +2,10 @@
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hutopy</title>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hutopy</title>
</head>
<body>

View File

@@ -0,0 +1,9 @@
export const REACTIONS = {
LIKE: 'Like',
DISLIKE: 'Dislike',
LOVE: 'Love',
HAHA: 'Haha',
WOW: 'Wow',
SAD: 'Sad',
ANGRY: 'Angry'
};

View File

@@ -64,12 +64,8 @@
<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>
<Reaction :content="content"></Reaction>
<v-btn
:class="{'comment-active': hasMessages}"
variant="plain"
@@ -152,6 +148,7 @@ import YoutubePlayer from './YoutubePlayer.vue';
import ImageViewer from './ImageViewer.vue';
import {useClient} from "@/plugins/api.js";
import {useAuthStore} from "@/stores/authStore.js";
import Reaction from "@/views/contents/Reaction.vue";
const props = defineProps({
content: {

View File

@@ -0,0 +1,327 @@
<script setup>
import { useUserStore } from "@/stores/userStore.js";
import { REACTIONS } from "@/Constants/Reactions.js";
import { computed, ref } from "vue";
import { useClient } from "@/plugins/api.js";
const userStore = useUserStore();
const props = defineProps({
content: {
type: Object,
required: true,
},
});
const contentId = computed(() => props.content.id);
const hasReacted = ref(false);
const currentReaction = ref(null);
const likeCount = ref(0);
const dislikeCount = ref(0);
const loveCount = ref(0);
const hahaCount = ref(0);
const wowCount = ref(0);
const sadCount = ref(0);
const angryCount = ref(0);
const menuVisible = ref(false);
const holdTimeout = ref(null);
const hideTimeout = ref(null);
const touchTimeout = ref(null);
initializeReactions();
async function reactToContent(reaction) {
const client = useClient();
if (!hasReacted.value) {
const request = {
ContentId: contentId.value,
reaction: reaction,
userId: userStore.user.id,
userName: `${userStore.user.firstName} ${userStore.user.lastName}`,
};
adjustReactionCount(reaction);
await client.post("/api/content/reaction/", request);
hasReacted.value = true;
console.log(`Added ${reaction} reaction to content.`);
} else if (reaction !== currentReaction.value) {
adjustReactionCount(currentReaction.value);
const requestRemove = {
ContentId: contentId.value,
userId: userStore.user.id,
};
await client.post("/api/content/reaction/remove", requestRemove);
const requestAdd = {
ContentId: contentId.value,
reaction: reaction,
userId: userStore.user.id,
userName: `${userStore.user.firstName} ${userStore.user.lastName}`,
};
adjustReactionCount(reaction);
await client.post("/api/content/reaction/", requestAdd);
console.log(`Changed reaction to ${reaction} on content.`);
} else {
const requestRemove = {
ContentId: contentId.value,
userId: userStore.user.id,
};
adjustReactionCount(reaction);
await client.post("/api/content/reaction/remove", requestRemove);
hasReacted.value = false;
console.log("Reaction to content removed.");
}
setTimeout(() => {
menuVisible.value = false;
}, 500);
}
function adjustReactionCount(newReaction) {
if (currentReaction.value === newReaction) {
switch (newReaction) {
case REACTIONS.LIKE:
if (likeCount.value > 0) likeCount.value--;
break;
case REACTIONS.DISLIKE:
if (dislikeCount.value > 0) dislikeCount.value--;
break;
case REACTIONS.LOVE:
if (loveCount.value > 0) loveCount.value--;
break;
case REACTIONS.HAHA:
if (hahaCount.value > 0) hahaCount.value--;
break;
case REACTIONS.WOW:
if (wowCount.value > 0) wowCount.value--;
break;
case REACTIONS.SAD:
if (sadCount.value > 0) sadCount.value--;
break;
case REACTIONS.ANGRY:
if (angryCount.value > 0) angryCount.value--;
break;
}
currentReaction.value = null;
hasReacted.value = false;
} else {
if (currentReaction.value) {
switch (currentReaction.value) {
case REACTIONS.LIKE:
if (likeCount.value > 0) likeCount.value--;
break;
case REACTIONS.DISLIKE:
if (dislikeCount.value > 0) dislikeCount.value--;
break;
case REACTIONS.LOVE:
if (loveCount.value > 0) loveCount.value--;
break;
case REACTIONS.HAHA:
if (hahaCount.value > 0) hahaCount.value--;
break;
case REACTIONS.WOW:
if (wowCount.value > 0) wowCount.value--;
break;
case REACTIONS.SAD:
if (sadCount.value > 0) sadCount.value--;
break;
case REACTIONS.ANGRY:
if (angryCount.value > 0) angryCount.value--;
break;
}
}
switch (newReaction) {
case REACTIONS.LIKE:
likeCount.value++;
break;
case REACTIONS.DISLIKE:
dislikeCount.value++;
break;
case REACTIONS.LOVE:
loveCount.value++;
break;
case REACTIONS.HAHA:
hahaCount.value++;
break;
case REACTIONS.WOW:
wowCount.value++;
break;
case REACTIONS.SAD:
sadCount.value++;
break;
case REACTIONS.ANGRY:
angryCount.value++;
break;
}
currentReaction.value = newReaction;
hasReacted.value = true;
}
}
function initializeReactions() {
const userReaction = props.content.reactions.find((x) => x.userId === userStore.user.id);
if (userReaction) {
currentReaction.value = userReaction.reaction;
hasReacted.value = true;
} else {
currentReaction.value = null;
hasReacted.value = false;
}
likeCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.LIKE).length;
dislikeCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.DISLIKE).length;
loveCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.LOVE).length;
hahaCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.HAHA).length;
wowCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.WOW).length;
sadCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.SAD).length;
angryCount.value = props.content.reactions.filter((x) => x.reaction === REACTIONS.ANGRY).length;
}
function showReactions() {
clearTimeout(hideTimeout.value);
menuVisible.value = true;
}
function hideReactions() {
hideTimeout.value = setTimeout(() => {
menuVisible.value = false;
}, 250);
}
function onTouchStart() {
touchTimeout.value = setTimeout(() => {
menuVisible.value = true;
}, 250);
}
function onTouchEnd() {
clearTimeout(touchTimeout.value);
}
function onMouseUp() {
clearTimeout(holdTimeout.value);
hideReactions();
}
function onMouseOver() {
if (!isMobileDevice()) {
showReactions();
}
}
function onMouseLeave() {
if (!isMobileDevice()) {
hideReactions();
}
}
function keepReactionMenuOpen(){
clearTimeout(hideTimeout.value);
}
function isMobileDevice() {
return window.innerWidth <= 800;
}
</script>
<template>
<div style="position: relative; display: inline-block;">
<v-menu
class="reaction-card"
v-model="menuVisible"
offset-y
:close-on-content-click="false"
transition="scale-transition"
:attach="$el"
>
<template v-slot:activator="{ on, attrs }">
<v-btn
v-bind="attrs"
:on="on"
variant="plain"
@touchstart="onTouchStart"
@touchend="onTouchEnd"
@mouseup="onMouseUp"
@mouseover="onMouseOver"
@mouseleave="onMouseLeave"
@click="reactToContent(REACTIONS.LIKE)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.LIKE}">mdi-thumb-up-outline</v-icon>
{{ likeCount }}
</v-btn>
</template>
<v-card
class="reaction-card"
@mouseover="keepReactionMenuOpen"
@mouseleave="hideReactions"
>
<v-btn
variant="plain"
@click="reactToContent(REACTIONS.DISLIKE)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.DISLIKE}">mdi-thumb-down-outline</v-icon>
</v-btn>
<v-btn
variant="plain"
@click="reactToContent(REACTIONS.LOVE)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.LOVE}">mdi-heart-outline</v-icon>
{{ loveCount }}
</v-btn>
<v-btn
variant="plain"
@click="reactToContent(REACTIONS.HAHA)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.HAHA}">mdi-emoticon-excited-outline</v-icon>
{{ hahaCount }}
</v-btn>
<v-btn
variant="plain"
@click="reactToContent(REACTIONS.WOW)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.WOW}">mdi-emoticon-happy-outline</v-icon>
{{ wowCount }}
</v-btn>
<v-btn
variant="plain"
@click="reactToContent(REACTIONS.SAD)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.SAD}">mdi-emoticon-sad-outline</v-icon>
{{ sadCount }}
</v-btn>
<v-btn
variant="plain"
@click="reactToContent(REACTIONS.ANGRY)"
>
<v-icon :class="{'active-icon': currentReaction === REACTIONS.ANGRY}">mdi-emoticon-angry-outline</v-icon>
{{ angryCount }}
</v-btn>
</v-card>
</v-menu>
</div>
</template>
<style scoped>
.reaction-card {
display: flex;
justify-content: space-around;
padding: 8px;
margin-top: -35px;
margin-left: 50px;
}
.active-icon {
color: blue;
stroke: blue;
}
</style>

View File

@@ -64,12 +64,8 @@
<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>
<Reaction :content="content"></Reaction>
<v-btn
:class="{'comment-active': hasMessages}"
variant="plain"
@@ -152,6 +148,7 @@ import YoutubePlayer from '../YoutubePlayer.vue';
import ImageViewer from '../ImageViewer.vue';
import {useClient} from "@/plugins/api.js";
import {useAuthStore} from "@/stores/authStore.js";
import Reaction from "@/views/contents/Reaction.vue";
const props = defineProps({
content: {

View File

@@ -64,12 +64,8 @@
<div class="px-1">
<div class="flex justify-around ">
<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>
<Reaction :content="content"></Reaction>
<v-btn
:class="{'comment-active': hasMessages}"
variant="plain"
@@ -152,6 +148,7 @@ import YoutubePlayer from '../YoutubePlayer.vue';
import ImageViewer from '../ImageViewer.vue';
import {useClient} from "@/plugins/api.js";
import {useAuthStore} from "@/stores/authStore.js";
import Reaction from "@/views/contents/Reaction.vue";
const props = defineProps({
content: {

View File

@@ -49,12 +49,7 @@
</div>
<div class="flex justify-around py-2">
<v-btn variant="plain" icon @click="likeContent">
<v-icon :color="'#313131'">mdi-thumb-up-outline</v-icon>
</v-btn>
<v-btn variant="plain" icon @click="dislikeContent">
<v-icon :color="'#000000'">mdi-thumb-down-outline</v-icon>
</v-btn>
<Reaction v-if="data" :content="data"></Reaction>
<donation-button v-if="data"
:color-border="data.colorMenu"
@@ -89,6 +84,7 @@ import MessageList from "@/views/messages/MessageList.vue";
import DonationButton from "@/views/creators/DonationButton.vue";
import {useClient} from "@/plugins/api.js";
import {useRoute} from 'vue-router';
import Reaction from "@/views/contents/Reaction.vue";
const data = ref(null);
const currentImageIndex = ref(0);
@@ -119,7 +115,6 @@ const fetchContentData = async (contentId) => {
try {
const response = await client.get(`/api/contents/${contentId}`);
data.value = response.data;
console.table(data.value)
} catch (error) {
console.error(`Error fetching content: ${error}`);
}

View File

@@ -53,12 +53,7 @@
</div>
<div class="flex justify-around py-2">
<v-btn variant="plain" icon @click="likeContent">
<v-icon :color="'#313131'">mdi-thumb-up-outline</v-icon>
</v-btn>
<v-btn variant="plain" icon @click="dislikeContent">
<v-icon :color="'#000000'">mdi-thumb-down-outline</v-icon>
</v-btn>
<Reaction v-if="data" :content="data"></Reaction>
<donation-button v-if="data"
:color-border="data.colorMenu"
@@ -93,6 +88,7 @@ import MessageList from "@/views/messages/MessageList.vue";
import DonationButton from "@/views/creators/DonationButton.vue";
import {useClient} from "@/plugins/api.js";
import {useRoute} from 'vue-router';
import Reaction from "@/views/contents/Reaction.vue";
const data = ref(null);
const currentImageIndex = ref(0);
@@ -123,7 +119,6 @@ const fetchContentData = async (contentId) => {
try {
const response = await client.get(`/api/contents/${contentId}`);
data.value = response.data;
console.table(data.value)
} catch (error) {
console.error(`Error fetching content: ${error}`);
}

View File

@@ -76,12 +76,12 @@ 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},
colorBorder: {required: true},
colorAccent: {required: true},
creatorId: {type: String, required: true},
creatorName: {type: String, required: true},
creatorLogo: {type: String, required: true},
iconColorClass: {type: String, default: 'text-black'}
creatorLogo: {required: true},
iconColorClass: {default: 'text-black'}
});
const colorBorder = computed(() => props.colorBorder)

View File

@@ -78,7 +78,7 @@
</template>
<v-btn variant="text" style="margin:0" @click="toggleLanguage">
{{ selectedLanguage === 'fr' ? 'Fr' : 'Eng' }}
{{ selectedLanguage === 'fr' ? 'Fr' : 'En' }}
</v-btn>
<div class="text-center">
@@ -160,7 +160,7 @@ const router = useRouter();
const searchQuery = ref("");
const showSearch = ref(false);
const selectedLanguage = ref(locale.value);
let selectedLanguage = ref(locale.value);
function toggleLanguage() {
const newLang = selectedLanguage.value === 'fr' ? 'en' : 'fr';
@@ -204,6 +204,18 @@ onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside);
});
function initializeLocale(){
const preferredLocale = localStorage.getItem('preferredLocale');
selectedLanguage = ref(preferredLocale === null ? locale.value : preferredLocale);
locale.value = selectedLanguage.value;
}
function changeLanguage(lang) {
locale.value = lang;
selectedLanguage.value = lang;
localStorage.setItem('preferredLocale', lang);
}
</script>
<style scoped>