Merged PR 111: Added Reaction component to use and fixed some warning from vue

Added Reaction component to use and fixed some warning from vue
This commit is contained in:
Dominic Villemure
2024-08-25 14:24:28 +00:00
9 changed files with 327 additions and 40 deletions

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://js.stripe.com https://accounts.google.com 'unsafe-eval';">
<title>Hutopy</title> <title>Hutopy</title>
</head> </head>

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

View File

@@ -0,0 +1,296 @@
<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}`,
};
await client.post("/api/content/reaction/", request);
adjustReactionCount(reaction, true);
hasReacted.value = true;
console.log(`Added ${reaction} reaction to content.`);
} else if (reaction !== currentReaction.value) {
adjustReactionCount(currentReaction.value, false);
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}`,
};
await client.post("/api/content/reaction/", requestAdd);
adjustReactionCount(reaction, true);
console.log(`Changed reaction to ${reaction} on content.`);
} else {
const requestRemove = {
ContentId: contentId.value,
userId: userStore.user.id,
};
await client.post("/api/content/reaction/remove", requestRemove);
adjustReactionCount(reaction, false);
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>mdi-thumb-up-outline</v-icon>
{{ likeCount }}
</v-btn>
</template>
<v-card
class="reaction-card"
@mouseover="keepReactionMenuOpen"
@mouseleave="hideReactions"
>
<v-btn variant="plain" icon @click="reactToContent(REACTIONS.LOVE)">
<v-icon>mdi-heart-outline</v-icon>
{{ loveCount }}
</v-btn>
<v-btn variant="plain" icon @click="reactToContent(REACTIONS.HAHA)">
<v-icon>mdi-emoticon-excited-outline</v-icon>
{{ hahaCount }}
</v-btn>
<v-btn variant="plain" icon @click="reactToContent(REACTIONS.WOW)">
<v-icon>mdi-emoticon-happy-outline</v-icon>
{{ wowCount }}
</v-btn>
<v-btn variant="plain" icon @click="reactToContent(REACTIONS.SAD)">
<v-icon>mdi-emoticon-sad-outline</v-icon>
{{ sadCount }}
</v-btn>
<v-btn variant="plain" icon @click="reactToContent(REACTIONS.ANGRY)">
<v-icon>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;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -53,12 +53,7 @@
</div> </div>
<div class="flex justify-around py-2"> <div class="flex justify-around py-2">
<v-btn variant="plain" icon @click="likeContent"> <Reaction v-if="data" :content="data"></Reaction>
<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" <donation-button v-if="data"
:color-border="data.colorMenu" :color-border="data.colorMenu"
@@ -93,6 +88,7 @@ import MessageList from "@/views/messages/MessageList.vue";
import DonationButton from "@/views/creators/DonationButton.vue"; import DonationButton from "@/views/creators/DonationButton.vue";
import {useClient} from "@/plugins/api.js"; import {useClient} from "@/plugins/api.js";
import {useRoute} from 'vue-router'; import {useRoute} from 'vue-router';
import Reaction from "@/views/contents/Reaction.vue";
const data = ref(null); const data = ref(null);
const currentImageIndex = ref(0); const currentImageIndex = ref(0);
@@ -123,7 +119,6 @@ const fetchContentData = async (contentId) => {
try { try {
const response = await client.get(`/api/contents/${contentId}`); const response = await client.get(`/api/contents/${contentId}`);
data.value = response.data; data.value = response.data;
console.table(data.value)
} catch (error) { } catch (error) {
console.error(`Error fetching content: ${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'; import {computed, onMounted, ref} from 'vue';
const props = defineProps({ const props = defineProps({
colorBorder: {type: String, required: true}, colorBorder: {required: true},
colorAccent: {type: String, required: true}, colorAccent: {required: true},
creatorId: {type: String, required: true}, creatorId: {type: String, required: true},
creatorName: {type: String, required: true}, creatorName: {type: String, required: true},
creatorLogo: {type: String, required: true}, creatorLogo: {required: true},
iconColorClass: {type: String, default: 'text-black'} iconColorClass: {default: 'text-black'}
}); });
const colorBorder = computed(() => props.colorBorder) const colorBorder = computed(() => props.colorBorder)