diff --git a/src/components/Exceptions/NotFoundPage.vue b/src/components/Exceptions/NotFoundPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..fceb97ac6adf0017e00025fa1703f5509117497c --- /dev/null +++ b/src/components/Exceptions/NotFoundPage.vue @@ -0,0 +1,47 @@ +<template> + <div class="container-fluid"> + <div class="row"> + <div class="col-md-12"> + <div class="error-template text-center"> + <h1> + Oi!</h1> + <h2 data-cy="404-error"> + 404 Ikke funnet</h2> + <div class="error-details"> + Beklager, det har oppstÃ¥tt en feil. Forespurt side ikke funnet! + </div> + <div class="error-actions"> + <BaseButton data-cy="to-home" button-text="Ta meg hjem" @click="home" /> + </div> + </div> + </div> + </div> + </div> +</template> + + +<script setup lang="ts"> +import { useRouter } from 'vue-router'; +import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue'; + +const router = useRouter(); + +const home = () => { + router.push('/'); // Assuming the root URL '/' is your home route +}; +</script> + + +<style scoped> +.error-template { + text-align: center; + /* Ensures all text and inline elements within are centered */ + display: flex; + flex-direction: column; + align-items: center; + /* Aligns child elements (which are block-level) centrally */ + justify-content: center; + /* Optional: if you want vertical centering */ + margin: 2rem; +} +</style> \ No newline at end of file diff --git a/src/components/Exceptions/UnauthorizedPage.vue b/src/components/Exceptions/UnauthorizedPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..99bfe00e211b79514f6fbb1b4ac8a2983b40018b --- /dev/null +++ b/src/components/Exceptions/UnauthorizedPage.vue @@ -0,0 +1,33 @@ +<template> + <body class="bg-dark text-white py-5"> + <div class="container py-5"> + <div class="row"> + <div class="col-md-2 text-center"> + <p><img src="../../assets/icons/danger.svg" alt="fare"> <br/>Statuskode: 403</p> + </div> + <div class="col-md-10"> + <h3>OOPS!!! Beklager...</h3> + <p>Beklager, din tilgang er nektet av sikkerhetsgrunner pÃ¥ serveren vÃ¥r og ogsÃ¥ vÃ¥re sensitive data.<br/>Vennligst gÃ¥ tilbake til startsiden for Ã¥ fortsette Ã¥ surfe.</p> + <BaseButton :button-text="'Ta meg hjem'" @click="home" /> + </div> + </div> + </div> + </body> +</template> + + +<script setup lang="ts"> +import { useRouter } from 'vue-router'; +import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue'; + +const router = useRouter(); + +const home = () => { + router.push('/'); +}; +</script> + + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/components/Friends/UserFriends.vue b/src/components/Friends/UserFriends.vue new file mode 100644 index 0000000000000000000000000000000000000000..1858195a3e056672c5f1f13120c03358b626b8e4 --- /dev/null +++ b/src/components/Friends/UserFriends.vue @@ -0,0 +1,605 @@ +<template> + <div class="container" style="margin-bottom: 3rem;"> + <h1 class="my-3">Dine venner</h1> + <div> + <button class="btn pull-right" @click="addNewFriends" id="addFriend">+ Legg til venn</button> + <div class="my-3"> + <button class="btn pages" @click="setupFriends" :class="{ 'active-tab': showFriends }"> + Dine venner + </button> + <button class="btn pages" @click="requestFriend" :class="{ 'active-tab': showRequests }"> + Venneforespørsler + </button> + </div> + </div> + <div v-if="showFriends"> + <div v-if="elementsInFriends"> + <div class="row"> + <div class="col-lg-3" v-for="friend in friends" :key="friend.id"> + <div class="card card-one"> + <div class="header"> + <div v-if="friend.profileImage" class="avatar"> + <img :src="'http://localhost:8080/api/images/' + friend.profileImage" alt=""> + </div> + <div v-else class="avatar"> + <img :src="'../src/assets/userprofile.png'" alt=""> + </div> + </div> + <h3><router-link to="" data-cy="navigateToFriend" href="#" class="btn stretched-link" + id="profileName" @click="navigateToFriend(friend.id)">{{ + friend.firstName }} {{ friend.lastName }}</router-link></h3> + <div class="desc">{{ friend.firstName }} {{ friend.lastName }}</div> + <div class="contacts"> + <a class="text removeFriend" data-bs-toggle="collapse" + :href="'#collapseExample' + friend.id" role="button" aria-expanded="false" + :aria-controls="'collapseExample' + friend.id"> + Se mer + </a> + <div class="collapse" :id="'collapseExample' + friend.id"> + <button class="btn btn-danger" @click="removeFriend(friend.id)"> + <h5><img src="@/assets/icons/remove-white.svg" style="width: 30px"> Fjern venn + </h5> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + <div v-else>Ingen venner</div> + </div> + <div v-else-if="showRequests" class="row"> + <div class="content-body"> + <div v-if="elementsInFriendRequest" id="requests"> + <div class="request" v-for="(friend) in friendRequests" :key="friend.id"> + <div v-if="friend.profileImage !== null"><img id="profilePicture" + :src="'http://localhost:8080/api/images/' + friend.profileImage" alt="bruker" + class="profile-photo-lg"></div> + <div v-else><img id="profilePicture" :src="'../src/assets/userprofile.png'" alt="bruker" + class="profile-photo-lg"></div> + <h2>{{ friend.firstName }}</h2> - <button class="btn btn-success mx-2" + @click="acceptRequest(friend.id)">Godta</button> + <button class="btn btn-danger" @click="rejectRequest(friend.id)">AvslÃ¥</button> + </div> + </div> + <div v-else>Ingen venneforespørsler</div> + </div> + </div> + <div v-if="showAddFriend" class="modal" tabindex="-1" role="dialog" + style="display:block; background-color: rgba(0,0,0,0.5);"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Legg til venn</h5> + <button type="button" class="close" @click="showAddFriend = false"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body d-flex justify-content-center align-items-center flex-column"> + <form class="col-md-10 d-flex justify-content-center align-items-center flex-row my-4" + id="searchBox" role="search" @submit.prevent="searchProfile(searchWord)"> + <input class="form-control me-2 custom-border" type="search" placeholder="Søk" + aria-label="Søk" v-model="searchWord"> + <button class="btn btn-success" type="submit">Søk</button> + </form> + <div class="col-md-12"> + <div class="people-nearby"> + <div v-for="user in searchedUsers" :key="user.id" class="nearby-user"> + <div class="row d-flex align-items-center"> + <div class="col-md-2 col-sm-2"> + <div v-if="user.profileImage !== null"><img id="profilePicture" + :src="'http://localhost:8080/api/images/' + user.profileImage" + alt="bruker" class="profile-photo-lg"></div> + <div v-else><img id="profilePicture" :src="'../src/assets/userprofile.png'" + alt="bruker" class="profile-photo-lg"></div> + </div> + <div class="col-md-7 col-sm-7"> + <h5><a href="#" class="profile-link" @click="toUserProfile(user.id)">{{ + user.firstName }}</a> + </h5> + </div> + <div class="col-md-3 col-sm-3"> + <button class="btn btn-primary pull-right" @click="addFriend(user.id)" + :disabled="friendRequestsSent[user.id]" + v-if="!friendRequestsSent[user.id]">Legg til venn</button> + <button class="btn btn-secondary pull-right" disabled + v-if="friendRequestsSent[user.id]">Forespørsel sendt</button> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + + + + +<script setup lang="ts"> +import { type Ref, ref, onMounted } from 'vue'; +import { useRouter } from 'vue-router'; +import { FriendService, UserService } from '@/api'; +import type { UserDTO } from '@/api'; + +const router = useRouter(); +const friends = ref(); +const showFriends = ref(true); +const showRequests = ref(false); +const showAddFriend = ref(false); +const friendRequests = ref([] as any); +const addFriends = ref([] as any); +const searchedUsers = ref([] as any); + +const friendRequestsSent: Ref<Record<number, boolean>> = ref({}); + +const searchWord = ref(""); + +const elementsInFriendRequest = ref(false); +const elementsInFriends = ref(false); + +const toUserProfile = (userId: number) => { + router.push('/profile/' + userId); +}; + +const searchProfile = async (searchTerm: string) => { + const userPayload = { + searchTerm: searchTerm as string, + filter: 'NON_FRIENDS' as string, + }; + try { + const response = await UserService.getUsersByNameAndFilter(userPayload); + searchedUsers.value = response; + console.log(response); + } catch (error) { + console.error('Failed to search for profile', error); + } +}; + +const addNewFriends = async () => { + const userPayload = { + amount: 6 as number, + filter: 'NON_FRIENDS' as string, + }; + try { + const response = await UserService.getRandomUsers(userPayload); + searchedUsers.value = response; + showAddFriend.value = true; + } catch (error) { + console.error('Failed to add friend', error); + } +}; + +async function addFriend(friendID: number) { + try { + await FriendService.addFriendRequest({ userId: friendID }); + // Use a spread to update the state and keep immutability + friendRequestsSent.value = { ...friendRequestsSent.value, [friendID]: true }; + } catch (error) { + console.error('Failed to send friend request', error); + } +} + +async function requestFriend() { + showRequests.value = true; + showFriends.value = false; + try { + const response = await FriendService.getFriendRequests(); + friendRequests.value = response; + elementsInFriendRequest.value = response.length > 0; + console.log("Friend requests: " + response); + } catch (error) { + console.error('Failed to fetch friend requests', error); + } +} + +const navigateToFriend = (friendID: number) => { + router.push('/profile/' + friendID); +}; + +const removeFriend = async (friendID: number) => { + try { + await FriendService.deleteFriendOrFriendRequest({ friendId: friendID }); + const responseFriends = await FriendService.getFriends(); + friends.value = responseFriends; + } catch (error) { + console.error('Failed to remove friend', error); + } +}; + +const setupFriends = async () => { + showFriends.value = true; + showRequests.value = false; + try { + const response = await FriendService.getFriends(); + friends.value = response; + elementsInFriends.value = response.length > 0; + console.log(response); + } catch (error) { + console.error('Failed to fetch friends', error); + } +}; + +const acceptRequest = async (requestID: number) => { + try { + await FriendService.acceptFriendRequest({ friendId: requestID }); + const responseRequest = await FriendService.getFriendRequests(); + friendRequests.value = responseRequest; + const responseFriends = await FriendService.getFriends(); + friends.value = responseFriends; + } catch (error) { + console.error('Failed to accept friend request', error); + } +}; + +const rejectRequest = async (requestID: number) => { + try { + await FriendService.deleteFriendOrFriendRequest({ friendId: requestID }); + const response = await FriendService.getFriendRequests(); + friendRequests.value = response; + } catch (error) { + console.error('Failed to reject friend request', error); + } +}; + +onMounted(() => { + setupFriends(); +}); +</script> + + +<style scoped> +body { + background-color: #f0f6ff; + color: #28384d; + +} + +/*social */ +.card-one { + position: relative; + width: 300px; + background: #fff; + box-shadow: 0 10px 7px -5px rgba(0, 0, 0, 0.4); +} + +.card { + margin-bottom: 35px; + padding-bottom: 1rem; + box-shadow: 0 10px 20px 0 rgba(26, 44, 57, 0.14); + border: none; +} + +.follower-wrapper li { + list-style-type: none; + color: #fff; + display: inline-block; + float: left; + margin-right: 20px; +} + +.social-profile { + color: #fff; +} + +.social-profile a { + color: #fff; +} + +.social-profile { + position: relative; + margin-bottom: 150px; +} + +.social-profile .user-profile { + position: absolute; + bottom: -75px; + width: 150px; + height: 150px; + border-radius: 50%; + left: 50px; +} + +.social-nav { + position: absolute; + bottom: 0; +} + +.social-prof { + color: #333; + text-align: center; +} + +.social-prof .wrapper { + width: 70%; + margin: auto; + margin-top: -100px; +} + +.social-prof img { + width: 150px; + height: 150px; + border-radius: 50%; + margin-bottom: 20px; + border: 5px solid #fff; + /*border: 10px solid #70b5e6ee;*/ +} + +.social-prof h3 { + font-size: 36px; + font-weight: 700; + margin-bottom: 0; +} + +.social-prof p { + font-size: 18px; +} + +.social-prof .nav-tabs { + border: none; +} + +.card .nav>li { + position: relative; + display: block; +} + +.card .nav>li>a { + position: relative; + display: block; + padding: 10px 15px; + font-weight: 300; + border-radius: 4px; +} + +.card .nav>li>a:focus, +.card .nav>li>a:hover { + text-decoration: none; + background-color: #eee; +} + +.card .s-nav>li>a.active { + text-decoration: none; + background-color: #3afe; + color: #fff; +} + +.text-blue { + color: #3afe; +} + +ul.friend-list { + margin: 0; + padding: 0; +} + +ul.friend-list li { + list-style-type: none; + display: flex; + align-items: center; +} + +ul.friend-list li:hover { + background: rgba(0, 0, 0, .1); + cursor: pointer; +} + +ul.friend-list .left img { + width: 45px; + height: 45px; + border-radius: 50%; + margin-right: 20px; +} + +ul.friend-list li { + padding: 10px; +} + +ul.friend-list .right h3 { + font-size: 16px; + font-weight: 700; + margin-bottom: 0; +} + +ul.friend-list .right p { + font-size: 11px; + color: #6c757d; + margin: 0; +} + +.social-timeline-card .dropdown-toggle::after { + display: none; +} + +.info-card h4 { + font-size: 15px; +} + +.info-card h2 { + font-size: 18px; + margin-bottom: 20px; +} + +.social-about .social-info { + font-size: 16px; + margin-bottom: 20px; +} + +.social-about p { + margin-bottom: 20px; +} + +.info-card i { + color: #3afe; +} + +.card-one { + position: relative; + width: 300px; + background: #fff; + box-shadow: 0 10px 7px -5px rgba(0, 0, 0, 0.4); +} + +.card-one .header { + position: relative; + width: 100%; + height: 60px; + background-color: rgba(7, 46, 74, 0.895); +} + +.card-one .header::before, +.card-one .header::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: inherit; +} + +.card-one .header::before { + -webkit-transform: skewY(-8deg); + transform: skewY(-8deg); + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} + +.card-one .header::after { + -webkit-transform: skewY(8deg); + transform: skewY(8deg); + -webkit-transform-origin: 0 100%; + transform-origin: 0 100%; +} + +.card-one .header .avatar { + position: absolute; + left: 50%; + top: 30px; + margin-left: -50px; + z-index: 5; + width: 100px; + height: 100px; + border-radius: 50%; + overflow: hidden; + background: #ccc; + border: 3px solid #fff; +} + +.card-one .header .avatar img { + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + width: 100px; + height: auto; +} + +.card-one h3 { + position: relative; + margin: 80px 0 30px; + text-align: center; +} + +.card-one h3::after { + content: ''; + position: absolute; + bottom: -15px; + left: 50%; + margin-left: -15px; + width: 30px; + height: 1px; + background: #000; +} + +.card-one .desc { + padding: 0 1rem 2rem; + text-align: center; + line-height: 1.5; + color: #777; +} + +#gallery li { + width: 24%; + float: left; + margin: 6px; + +} + +.removeFriend { + text-wrap: nowrap; +} + +.contacts { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +#profileName { + font-size: 1.5rem; + font-weight: 600; + width: 100%; +} + +#requests { + display: flex; + flex-direction: column; + align-items: center; +} + +.request { + display: flex; + justify-content: center; + align-items: center; + margin: 1rem; +} + +#profilePicture { + width: 70px; + height: 70px; + border-radius: 50%; + margin-right: 1rem; + border: 2px solid #000; +} + +.modal-content { + padding: 1rem; +} + +.modal-header { + margin-bottom: 5px; +} + +.pages { + border-bottom: 1px solid #000; + border-radius: 0px; + margin: 0px 5px; +} + +.pages { + border-bottom: 2px solid #000; + /* default border */ + border-radius: 0px; + margin: 0px 5px; +} + +.active-tab { + border-bottom: 4px solid #000; + /* thicker border when active */ +} + +#addFriend { + background-color: #084766; + color: white; +} + +#addFriend:hover { + background-color: #003b58f5; +} +</style> \ No newline at end of file diff --git a/src/components/Leaderboard/LeaderboardRank.vue b/src/components/Leaderboard/LeaderboardRank.vue new file mode 100644 index 0000000000000000000000000000000000000000..07f88f352b3f757e9683b273cdeb8938ab923284 --- /dev/null +++ b/src/components/Leaderboard/LeaderboardRank.vue @@ -0,0 +1,209 @@ +<template> + <br> + <div id="dropdownContainer"> + <h1 class="box">Poengtavle</h1> + </div> + <div id="content"> + <div id="dropdownContainer"> + <div class="box"> + <div class="btn-group-vertical" id="radioContainer" role="group" + aria-label="Vertikal radio knappgruppe"> + <input type="radio" class="btn-check" name="vbtn-radio" id="vbtn-radio1" autocomplete="off" checked> + <label class="btn btn-outline-primary" for="vbtn-radio1" @click="global"><img src="../../assets/globe.png" style="width: 60px" alt="globus"> Global</label> + <input type="radio" class="btn-check" name="vbtn-radio" id="vbtn-radio2" autocomplete="off"> + <label data-cy="friends-leaderboard-btn" class="btn btn-outline-primary" + for="vbtn-radio2" + @click="friends"><img src="../../assets/friends.png" style="width: 60px" alt="venner"> Venner</label> + </div> + </div> + </div> + <main> + <div id="leaderboard"> + <h1><img src="../../assets/items/pigcoin.png" style="width: 2rem" alt="pig coin"> Totale poeng</h1> + <Leaderboard data-cy="total-points-board" :leaderboard="pointsLeaderboardData" + :leaderboardExtra="pointsLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> + </div> + <div id="leaderboard"> + <h1><img src="../../assets/icons/fire.png" style="width: 2rem" alt="ild"> NÃ¥værende rekke</h1> + <Leaderboard data-cy="current-points-board" :leaderboard="currentLeaderboardData" + :leaderboardExtra="currentLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> + </div> + <div id="leaderboard"> + <h1><img src="../../assets/icons/fire.png" style="width: 2rem" alt="ild"> Høyeste rekke</h1> + <Leaderboard data-cy="streak-board" :leaderboard="streakLeaderboardData" + :leaderboardExtra="streakLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> + </div> + </main> + </div> + <div id="communityContainer"> + <h1>Totale poeng opptjent som et fellesskap</h1> + <h2>1000000 <img src="../../assets/items/v-buck.png" style="width: 2rem" alt="alt"></h2> + </div> +</template> + + +<script setup lang="ts"> +import { onMounted, ref } from 'vue'; +import { useRouter } from 'vue-router'; +import Leaderboard from '@/components/Leaderboard/LeaderboardTable.vue'; +import { LeaderboardService } from '@/api'; + +let streakLeaderboardData = ref([] as any); +let currentLeaderboardData = ref([] as any); +let pointsLeaderboardData = ref([] as any); + +let streakLeaderboardDataExtra = ref([] as any); +let currentLeaderboardDataExtra = ref([] as any); +let pointsLeaderboardDataExtra = ref([] as any); + +const router = useRouter(); + +async function fetchQuizData() { + await global(); +} + +onMounted(() => { + fetchQuizData(); +}); + +async function global() { + let globalPoints = await LeaderboardService.getLeaderboard({ + type: "TOTAL_POINTS", + filter: "GLOBAL", + }); + let globalStreak = await LeaderboardService.getLeaderboard({ + type: "TOP_STREAK", + filter: "GLOBAL", + }); + let globalCurrentStreak = await LeaderboardService.getLeaderboard({ + type: "CURRENT_STREAK", + filter: "GLOBAL", + }); + let globalPointsYou = await LeaderboardService.getSurrounding({ + type: "TOTAL_POINTS", + filter: "GLOBAL", + entryCount: 2, + }); + let globalStreakYou = await LeaderboardService.getSurrounding({ + type: "TOP_STREAK", + filter: "GLOBAL", + entryCount: 2, + }); + let globalCurrentStreakYou = await LeaderboardService.getSurrounding({ + type: "CURRENT_STREAK", + filter: "GLOBAL", + entryCount: 2, + }); + + pointsLeaderboardData.value = globalPoints.entries; + currentLeaderboardData.value = globalCurrentStreak.entries; + streakLeaderboardData.value = globalStreak.entries; + + pointsLeaderboardDataExtra.value = globalPointsYou.entries; + currentLeaderboardDataExtra.value = globalCurrentStreakYou.entries; + streakLeaderboardDataExtra.value = globalStreakYou.entries; +} + +async function friends() { + let friendsPoints = await LeaderboardService.getLeaderboard({ + type: "TOTAL_POINTS", + filter: "FRIENDS", + }); + let friendsStreak = await LeaderboardService.getLeaderboard({ + type: "TOP_STREAK", + filter: "FRIENDS", + }); + let friendsCurrentStreak = await LeaderboardService.getLeaderboard({ + type: "CURRENT_STREAK", + filter: "FRIENDS", + }); + let friendsPointsYou = await LeaderboardService.getSurrounding({ + type: "TOTAL_POINTS", + filter: "FRIENDS", + entryCount: 2, + }); + let friendsStreakYou = await LeaderboardService.getSurrounding({ + type: "TOP_STREAK", + filter: "FRIENDS", + entryCount: 2, + }); + let friendsCurrentStreakYou = await LeaderboardService.getSurrounding({ + type: "CURRENT_STREAK", + filter: "FRIENDS", + entryCount: 2, + }); + + + pointsLeaderboardData.value = friendsPoints.entries; + currentLeaderboardData.value = friendsCurrentStreak.entries; + streakLeaderboardData.value = friendsStreak.entries; + + pointsLeaderboardDataExtra.value = friendsPointsYou.entries; + currentLeaderboardDataExtra.value = friendsStreakYou.entries; + streakLeaderboardDataExtra.value = friendsCurrentStreakYou.entries; +} + +const navigateToUserProfile = (userId: number) => { + router.push({ name: 'user', params: { id: userId } }); +}; +</script> + +<style scoped> +main { + margin-bottom: 4rem; + width: 80%; + display: flex; + justify-content: space-around; + align-items: start; + flex-wrap: wrap; + flex-direction: row; +} + +#leaderboard { + width: 400px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-bottom: 3rem; +} + +#content { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; +} + +.box { + width: 90%; +} + +h1 { + font-weight: 700; + margin-bottom: 1rem; +} + +#dropdownContainer { + display: flex; + justify-content: center; + margin-bottom: 2rem; +} + +#radioContainer { + display: flex; + justify-content: center; + margin-bottom: 2rem; + width: 100%; + margin-top: 3.6rem; +} + +#communityContainer { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin-bottom: 5rem; +} + +</style> \ No newline at end of file diff --git a/src/components/Login/ChangePassword.vue b/src/components/Login/ChangePassword.vue new file mode 100644 index 0000000000000000000000000000000000000000..2e15299f39c4aca4bc6d87248ed6c72a13d49a8d --- /dev/null +++ b/src/components/Login/ChangePassword.vue @@ -0,0 +1,171 @@ +<template> + <div class="containers"> + <div class="box"> + <div class="container-fluid"> + <div class="container-fluid d-flex justify-content-center align-items-center flex-column mt-5"> + <h1>Opprett nytt passord</h1> + </div> + <form ref="formRef" id="loginForm" @submit.prevent="handleSubmit" novalidate> + + <BaseInput :model-value="newPassword" + @input-change-event="handlePasswordInputEvent" + id="passwordInput" + input-id="password" + type="password" + pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" + label="Passord" + placeholder="Skriv inn ditt passord" + invalid-message="Passordet mÃ¥ være mellom 4 og 16 tegn og inneholde én stor bokstav, liten bokstav og et tall" + /> + + <BaseInput :model-value="confirmPassword" + @input-change-event="handleConfirmPasswordInputEvent" + id="confirmPasswordInput" + input-id="password" + type="password" + pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" + label="Bekreft Passord" + placeholder="Skriv inn ditt passord" + invalid-message="Passordet mÃ¥ være mellom 4 og 16 tegn og inneholde én stor bokstav, liten bokstav og et tall" + /> + + <p class="text-danger" data-cy="error">{{ errorMsg }}</p> + <p v-if="!samePasswords" class="text-danger">Passordene er ikke like</p> + <BaseButton id="confirmButton" type="submit" @click="handleSubmit" :disabled="isSubmitting" button-text="Oppdater passordet"></BaseButton> + + <SignUpLink/> + </form> + </div> + <!--<div class="row justify-content-center"> + <div class="col-lg-5"> + <div class="card shadow-lg border-0 rounded-lg mt-5"> + <div class="card-header"> + <h3 class="text-center font-weight-light my-4">Password Recovery</h3> + </div> + <div class="card-body"> + <div class="small mb-3 text-muted">Enter the new password for your account</div> + <form @submit.prevent="submitForm"> + <div class="form-floating mb-3"> + <input v-model="newPassword" class="form-control" id="newPassword" type="password" + placeholder="New Password" required> + <label for="newPassword">Enter your new password</label> + </div> + <div class="form-floating mb-3"> + <input v-model="confirmPassword" class="form-control" id="confirmPassword" + type="password" placeholder="Confirm Password" required> + <label for="confirmPassword">Confirm your new password</label> + </div> + <div class="errorMsg">{{ errormsg }}</div> + <div class="d-flex align-items-center justify-content-between mt-4 mb-0"> + <router-link to="/login" class="small">Return to login</router-link> + <button class="btn btn-primary" type="submit">Confirm Password</button> + </div> + </form> + </div> + <div class="card-footer text-center py-3"> + <div class="small"><router-link to="/sign-up">Need an account? Sign up!</router-link></div> + </div> + </div> + </div> + </div>--> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; +import { UserService } from '@/api'; +import SignUpLink from '@/components/SignUp/SignUpLink.vue' +import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue' +import BaseInput from '@/components/BaseComponents/Input/BaseInput.vue' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' + +const router = useRouter(); +const route = useRoute(); + +const token = route.params.token; + +const newPassword = ref(''); +const confirmPassword = ref(''); +const formRef = ref() +let samePasswords = ref(true) +let errorMsg = ref(''); +const isSubmitting = ref(false); + +const handlePasswordInputEvent = (newValue: any) => { + newPassword.value = newValue +} + +const handleConfirmPasswordInputEvent = (newValue: any) => { + confirmPassword.value = newValue +} + +const handleSubmit = async () => { + if (isSubmitting.value) return; + isSubmitting.value = true; + + samePasswords.value = (newPassword.value === confirmPassword.value) + formRef.value.classList.add("was-validated") + + const form = formRef.value; + if (form.checkValidity()) { + if (samePasswords.value) { + try { + const resetPassword = { + password: newPassword.value, + token: token as string, + }; + await UserService.confirmPasswordReset({ requestBody: resetPassword }); + router.push('/login'); + } catch (error) { + errorMsg.value = handleUnknownError(error); + } + } + } + isSubmitting.value = false; +}; + +</script> + +<style scoped> +.containers { + background: url('@/assets/wave.svg'); + background-repeat: no-repeat; + background-size: cover; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.box { + background-color: white; + border-radius: 1rem; + max-width: 450px; + padding: 0 3rem 1rem 3rem; + box-shadow: rgba(57, 57, 63, 0.5) 0px 1px 20px 0px; +} + +h1 { + font-size: 2rem; + font-weight: bold; +} + +.container-fluid { + max-width: 450px; +} + +#loginForm { + display: flex; + flex-direction: column; + align-items: center; +} + +#passwordInput, +#confirmPasswordInput { + margin: 1rem 10rem; + width: 100%; +} +</style> \ No newline at end of file diff --git a/src/components/Login/ForgottenPassword.vue b/src/components/Login/ForgottenPassword.vue new file mode 100644 index 0000000000000000000000000000000000000000..6d5e7fc808c4eb43be34b7550cf0b4e78d8fa82d --- /dev/null +++ b/src/components/Login/ForgottenPassword.vue @@ -0,0 +1,103 @@ +<template> + <div class="containers"> + <div class="box"> + <h1 class="title">Tilbakestill passord</h1> + <p>Fyll inn e-posten din, sÃ¥ sender vi deg instruksjoner for Ã¥ tilbakestille passordet ditt.</p> + <form @submit.prevent="submitForm" id="resetForm" ref="formRef" novalidate> + <div class="form-floating inputBox"> + <input v-model="email" class="form-control" id="inputEmail" type="email" + placeholder="name@example.com" required> + <label for="emailInput">Skriv inn din e-post</label> + </div> + + <div v-if="errorMessage" class="text-danger"> + {{ errorMessage }} + </div> + <div v-else class="text-success"> + {{ confirmationMessage }} + </div> + <BaseButton id="confirmButton" type="submit" :disabled="isSubmitting" button-text="Send e-post"></BaseButton> + + <div class="login-link"> + <Router-Link to="/login" class="small">GÃ¥ tilbake</Router-Link> + </div> + </form> + </div> + </div> + </template> + + <script setup lang="ts"> + import { ref } from 'vue'; + import { UserService } from '@/api'; + import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue' + + const formRef = ref() + const form = formRef.value; + const email = ref(''); + const confirmationMessage = ref(''); + const errorMessage = ref(''); + const isSubmitting = ref(false); + + const submitForm = async () => { + if (isSubmitting.value) return; + isSubmitting.value = true; + + formRef.value.classList.add("was-validated") + + try { + await UserService.resetPassword({ requestBody: email.value }); + confirmationMessage.value = 'An email has been sent to your email address with a link to reset your password.'; + errorMessage.value = ''; + } catch (error) { + errorMessage.value = 'Failed to send email. Please try again.'; + confirmationMessage.value = ''; + } + isSubmitting.value = false; + }; + </script> + + <style scoped> + .containers { + background: url('@/assets/wave.svg'); + background-size: cover; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + } + + .box { + background-color: white; + border-radius: 1rem; + width: 100%; + max-width: 450px; + padding: 2rem; + box-shadow: rgba(57, 57, 63, 0.5) 0px 1px 20px 0px; + text-align: center; + } + + h1 { + font-size: 2rem; + font-weight: bold; + text-align: center; + } + + #resetForm { + display: flex; + flex-direction: column; + align-items: center; + } + + .login-link { + width: 100%; + font-size: 14px; + margin-top: 10px; + text-align: center; + } + + .inputBox { + width: 100%; + margin: 20px; + } + </style> + \ No newline at end of file diff --git a/src/components/Shop/ItemShop.vue b/src/components/Shop/ItemShop.vue new file mode 100644 index 0000000000000000000000000000000000000000..15f17c636da19d0e18deb7e1f627027ff310a7ac --- /dev/null +++ b/src/components/Shop/ItemShop.vue @@ -0,0 +1,171 @@ +<template> + <div id="background"> + <br> + <div id="dropdownContainer"> + <h1 class="box">Butikk</h1> + </div> + <div class="container d-flex justify-content-center"> + <div class="row col-md-10"> + <div class="col-md-12"> + <h1>Stash</h1> + <div class="category row justify-content-between mb-5 m-2"> + <div class="card text-center" style="width: 16rem; border: none"> + <img src="../../assets/items/adfree.png" class="card-img-top" alt="..."> + <div class="card-body"> + <h5 class="card-title">Adfree</h5> + <button type="button" class="btn btn-primary" id="buttonStyle" @click="buyNoAds"> + +35kr</button> + </div> + </div> + <div class="card text-center" style="width: 16rem; border: none"> + <img src="../../assets/items/piggybank.webp" class="card-img-top" alt="..."> + <div class="card-body"> + <h5 class="card-title">Premium</h5> + <button type="button" class="btn btn-primary" id="buttonStyle" + @click="buyPremium">+50kr</button> + </div> + </div> + </div> + </div> + <div class="col-md-12"> + <h1>Items</h1> + <div class="category row justify-content-between mb-5 m-2"> + <div v-for="product in products" :key="product.id" class="card text-center" + style="width: 16rem; border: none"> + <img :src="`http://localhost:8080/api/images/${product.imageId}`" class="card-img-top" + alt="..." /> + <div class="card-body"> + <h5 class="card-title">{{ product.itemName }}</h5> + <ShopButton v-if="!product.alreadyBought" :button-text="product.price" + @click="buyItem(product.id)"></ShopButton> + <p v-else>Owned</p> + </div> + </div> + </div> + </div> + <div class="col-md-12"> + <h1>Stash</h1> + <div class="category row justify-content-between mb-5 m-2"> + <div class="card text-center" style="width: 16rem; border: none"> + <img src="../../assets/items/coffee.jpg" class="card-img-top" alt="..."> + <div class="card-body"> + <h5 class="card-title">Free Coffee</h5> + <ShopButton button-text="500"></ShopButton> + </div> + </div> + <div class="card text-center" style="width: 16rem; border: none"> + <img src="../../assets/items/viaplay.jpg" class="card-img-top" alt="..."> + <div class="card-body"> + <h5 class="card-title">1 Month Viaplay</h5> + <ShopButton button-text="10000"></ShopButton> + </div> + </div> + <div class="card text-center" style="width: 16rem; border: none"> + <img src="../../assets/items/pirbad.png" class="card-img-top" alt="..."> + <div class="card-body"> + <h5 class="card-title">-10% rabatt</h5> + <ShopButton button-text="1000"></ShopButton> + </div> + </div> + </div> + </div> + + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import ShopButton from '@/components/Shop/ShopButton.vue'; +import { ref, onMounted } from 'vue'; +import { UserService } from '@/api'; +import { useUserInfoStore } from '@/stores/UserStore'; +import { ItemService } from '@/api'; + +const products = ref([] as any); + +const getStore = async () => { + const response = await ItemService.getStore(); + products.value = response; + console.log(response); +} + +const buyItem = async (itemId: number) => { + try { + const response = await ItemService.buyItem({ itemId: itemId }); + console.log(response); + const responseStore = await ItemService.getStore(); + products.value = responseStore; + } catch (error) { + console.log(error); + } +} + +const buyPremium = async () => { + try { + const response = await UserService.updateSubscriptionLevel({ subscriptionLevel: 'PREMIUM' }); + useUserInfoStore().setUserInfo({ + subscriptionLevel: 'PREMIUM', + }) + } catch (error) { + console.log(error); + } +} + +const buyNoAds = async () => { + try { + const response = await UserService.updateSubscriptionLevel({ subscriptionLevel: 'NO_ADS' }); + useUserInfoStore().setUserInfo({ + subscriptionLevel: 'NO_ADS', + }) + } catch (error) { + console.log(error); + } +} + +onMounted(() => { + getStore(); +}) +</script> + +<style scoped> +.card { + box-shadow: none; + margin: 10px; + border-radius: 8px; + padding-left: 5px; + padding-right: 5px; + /* Rounded corners */ +} + +.box { + width: 90%; + justify-content: center; + text-align: center; + font-size: 5rem; + font-weight: 700; +} + +.card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.card-body { + height: 100px; + padding: 5px; +} + +.col-md-12 { + border-bottom: 2px solid #000000; +} + +#dropdownContainer { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 2rem; +} + +#background { +} +</style> \ No newline at end of file diff --git a/src/views/Authentication/ChangePasswordView.vue b/src/views/Authentication/ChangePasswordView.vue index 2e15299f39c4aca4bc6d87248ed6c72a13d49a8d..53414e404c66a69ff7662b9fa187492ee613f151 100644 --- a/src/views/Authentication/ChangePasswordView.vue +++ b/src/views/Authentication/ChangePasswordView.vue @@ -1,171 +1,7 @@ <template> - <div class="containers"> - <div class="box"> - <div class="container-fluid"> - <div class="container-fluid d-flex justify-content-center align-items-center flex-column mt-5"> - <h1>Opprett nytt passord</h1> - </div> - <form ref="formRef" id="loginForm" @submit.prevent="handleSubmit" novalidate> - - <BaseInput :model-value="newPassword" - @input-change-event="handlePasswordInputEvent" - id="passwordInput" - input-id="password" - type="password" - pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" - label="Passord" - placeholder="Skriv inn ditt passord" - invalid-message="Passordet mÃ¥ være mellom 4 og 16 tegn og inneholde én stor bokstav, liten bokstav og et tall" - /> - - <BaseInput :model-value="confirmPassword" - @input-change-event="handleConfirmPasswordInputEvent" - id="confirmPasswordInput" - input-id="password" - type="password" - pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,16}" - label="Bekreft Passord" - placeholder="Skriv inn ditt passord" - invalid-message="Passordet mÃ¥ være mellom 4 og 16 tegn og inneholde én stor bokstav, liten bokstav og et tall" - /> - - <p class="text-danger" data-cy="error">{{ errorMsg }}</p> - <p v-if="!samePasswords" class="text-danger">Passordene er ikke like</p> - <BaseButton id="confirmButton" type="submit" @click="handleSubmit" :disabled="isSubmitting" button-text="Oppdater passordet"></BaseButton> - - <SignUpLink/> - </form> - </div> - <!--<div class="row justify-content-center"> - <div class="col-lg-5"> - <div class="card shadow-lg border-0 rounded-lg mt-5"> - <div class="card-header"> - <h3 class="text-center font-weight-light my-4">Password Recovery</h3> - </div> - <div class="card-body"> - <div class="small mb-3 text-muted">Enter the new password for your account</div> - <form @submit.prevent="submitForm"> - <div class="form-floating mb-3"> - <input v-model="newPassword" class="form-control" id="newPassword" type="password" - placeholder="New Password" required> - <label for="newPassword">Enter your new password</label> - </div> - <div class="form-floating mb-3"> - <input v-model="confirmPassword" class="form-control" id="confirmPassword" - type="password" placeholder="Confirm Password" required> - <label for="confirmPassword">Confirm your new password</label> - </div> - <div class="errorMsg">{{ errormsg }}</div> - <div class="d-flex align-items-center justify-content-between mt-4 mb-0"> - <router-link to="/login" class="small">Return to login</router-link> - <button class="btn btn-primary" type="submit">Confirm Password</button> - </div> - </form> - </div> - <div class="card-footer text-center py-3"> - <div class="small"><router-link to="/sign-up">Need an account? Sign up!</router-link></div> - </div> - </div> - </div> - </div>--> - </div> - </div> + <ChangePassword/> </template> <script setup lang="ts"> -import { ref } from 'vue'; -import { useRouter, useRoute } from 'vue-router'; -import { UserService } from '@/api'; -import SignUpLink from '@/components/SignUp/SignUpLink.vue' -import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue' -import BaseInput from '@/components/BaseComponents/Input/BaseInput.vue' -import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' - -const router = useRouter(); -const route = useRoute(); - -const token = route.params.token; - -const newPassword = ref(''); -const confirmPassword = ref(''); -const formRef = ref() -let samePasswords = ref(true) -let errorMsg = ref(''); -const isSubmitting = ref(false); - -const handlePasswordInputEvent = (newValue: any) => { - newPassword.value = newValue -} - -const handleConfirmPasswordInputEvent = (newValue: any) => { - confirmPassword.value = newValue -} - -const handleSubmit = async () => { - if (isSubmitting.value) return; - isSubmitting.value = true; - - samePasswords.value = (newPassword.value === confirmPassword.value) - formRef.value.classList.add("was-validated") - - const form = formRef.value; - if (form.checkValidity()) { - if (samePasswords.value) { - try { - const resetPassword = { - password: newPassword.value, - token: token as string, - }; - await UserService.confirmPasswordReset({ requestBody: resetPassword }); - router.push('/login'); - } catch (error) { - errorMsg.value = handleUnknownError(error); - } - } - } - isSubmitting.value = false; -}; - -</script> - -<style scoped> -.containers { - background: url('@/assets/wave.svg'); - background-repeat: no-repeat; - background-size: cover; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.box { - background-color: white; - border-radius: 1rem; - max-width: 450px; - padding: 0 3rem 1rem 3rem; - box-shadow: rgba(57, 57, 63, 0.5) 0px 1px 20px 0px; -} - -h1 { - font-size: 2rem; - font-weight: bold; -} - -.container-fluid { - max-width: 450px; -} - -#loginForm { - display: flex; - flex-direction: column; - align-items: center; -} - -#passwordInput, -#confirmPasswordInput { - margin: 1rem 10rem; - width: 100%; -} -</style> \ No newline at end of file +import ChangePassword from '@/components/Login/ChangePassword.vue' +</script> \ No newline at end of file diff --git a/src/views/Authentication/ForgottenPasswordView.vue b/src/views/Authentication/ForgottenPasswordView.vue index 81b884625b896fc3159458adcbb4bc097427f6cd..ef2cd9f18081b53d8943d238cb8ad6a827f27d1d 100644 --- a/src/views/Authentication/ForgottenPasswordView.vue +++ b/src/views/Authentication/ForgottenPasswordView.vue @@ -1,102 +1,7 @@ <template> - <div class="containers"> - <div class="box"> - <h1 class="title">Tilbakestill passord</h1> - <p>Fyll inn e-posten din, sÃ¥ sender vi deg instruksjoner for Ã¥ tilbakestille passordet ditt.</p> - <form @submit.prevent="submitForm" id="resetForm" ref="formRef" novalidate> - <div class="form-floating inputBox"> - <input v-model="email" class="form-control" id="inputEmail" type="email" - placeholder="name@example.com" required> - <label for="emailInput">Skriv inn din e-post</label> - </div> - - <div v-if="errorMessage" class="text-danger"> - {{ errorMessage }} - </div> - <div v-else class="text-success"> - {{ confirmationMessage }} - </div> - <BaseButton id="confirmButton" type="submit" :disabled="isSubmitting" button-text="Send e-post"></BaseButton> - - <div class="login-link"> - <Router-Link to="/login" class="small">GÃ¥ tilbake</Router-Link> - </div> - </form> - </div> - </div> + <ForgottenPassword/> </template> <script setup lang="ts"> -import { ref } from 'vue'; -import { UserService } from '@/api'; -import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue' - -const formRef = ref() -const form = formRef.value; -const email = ref(''); -const confirmationMessage = ref(''); -const errorMessage = ref(''); -const isSubmitting = ref(false); - -const submitForm = async () => { - if (isSubmitting.value) return; - isSubmitting.value = true; - - formRef.value.classList.add("was-validated") - - try { - await UserService.resetPassword({ requestBody: email.value }); - confirmationMessage.value = 'An email has been sent to your email address with a link to reset your password.'; - errorMessage.value = ''; - } catch (error) { - errorMessage.value = 'Failed to send email. Please try again.'; - confirmationMessage.value = ''; - } - isSubmitting.value = false; -}; -</script> - -<style scoped> -.containers { - background: url('@/assets/wave.svg'); - background-size: cover; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; -} - -.box { - background-color: white; - border-radius: 1rem; - width: 100%; - max-width: 450px; - padding: 2rem; - box-shadow: rgba(57, 57, 63, 0.5) 0px 1px 20px 0px; - text-align: center; -} - -h1 { - font-size: 2rem; - font-weight: bold; - text-align: center; -} - -#resetForm { - display: flex; - flex-direction: column; - align-items: center; -} - -.login-link { - width: 100%; - font-size: 14px; - margin-top: 10px; - text-align: center; -} - -.inputBox { - width: 100%; - margin: 20px; -} -</style> +import ForgottenPassword from '@/components/Login/ForgottenPassword.vue' +</script> \ No newline at end of file diff --git a/src/views/Authentication/LoginView.vue b/src/views/Authentication/LoginView.vue index a4bb674beddad0aba2764790e54b62a7dcb370e4..8e98a1cec4e1285528b46c7b5aa96604d45ef030 100644 --- a/src/views/Authentication/LoginView.vue +++ b/src/views/Authentication/LoginView.vue @@ -1,13 +1,7 @@ -<script setup lang="ts"> -import Footer from '@/components/BaseComponents/BaseFooter.vue' -import Menu from '@/components/BaseComponents/NavBar.vue' -import Login from '@/components/Login/LoginParent.vue' -</script> - <template> <Login/> </template> -<style scoped> - -</style> \ No newline at end of file +<script setup lang="ts"> +import Login from '@/components/Login/LoginParent.vue' +</script> \ No newline at end of file diff --git a/src/views/Authentication/SignUpView.vue b/src/views/Authentication/SignUpView.vue index b6b2f2e951c3c776ae746a3779e73301bb6c6f48..7fdad6614a10dccd5b45a80c1786cfa928cd89f0 100644 --- a/src/views/Authentication/SignUpView.vue +++ b/src/views/Authentication/SignUpView.vue @@ -1,11 +1,7 @@ -<script setup lang="ts"> -import SignUp from '@/components/SignUp/SignUp.vue' -</script> - <template> <SignUp/> </template> -<style scoped> - -</style> \ No newline at end of file +<script setup lang="ts"> +import SignUp from '@/components/SignUp/SignUp.vue' +</script> \ No newline at end of file diff --git a/src/views/Exception/NotFoundView.vue b/src/views/Exception/NotFoundView.vue index fceb97ac6adf0017e00025fa1703f5509117497c..d86aab5d9cc9a72056091020e461f416b11639f6 100644 --- a/src/views/Exception/NotFoundView.vue +++ b/src/views/Exception/NotFoundView.vue @@ -1,47 +1,7 @@ <template> - <div class="container-fluid"> - <div class="row"> - <div class="col-md-12"> - <div class="error-template text-center"> - <h1> - Oi!</h1> - <h2 data-cy="404-error"> - 404 Ikke funnet</h2> - <div class="error-details"> - Beklager, det har oppstÃ¥tt en feil. Forespurt side ikke funnet! - </div> - <div class="error-actions"> - <BaseButton data-cy="to-home" button-text="Ta meg hjem" @click="home" /> - </div> - </div> - </div> - </div> - </div> + <NotFoundPage/> </template> - <script setup lang="ts"> -import { useRouter } from 'vue-router'; -import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue'; - -const router = useRouter(); - -const home = () => { - router.push('/'); // Assuming the root URL '/' is your home route -}; -</script> - - -<style scoped> -.error-template { - text-align: center; - /* Ensures all text and inline elements within are centered */ - display: flex; - flex-direction: column; - align-items: center; - /* Aligns child elements (which are block-level) centrally */ - justify-content: center; - /* Optional: if you want vertical centering */ - margin: 2rem; -} -</style> \ No newline at end of file + import NotFoundPage from '@/components/Exceptions/NotFoundPage.vue'; +</script> \ No newline at end of file diff --git a/src/views/Exception/UnauthorizedView.vue b/src/views/Exception/UnauthorizedView.vue index 99bfe00e211b79514f6fbb1b4ac8a2983b40018b..412c8d2319480ed0c12661441cd85db78f8e8f75 100644 --- a/src/views/Exception/UnauthorizedView.vue +++ b/src/views/Exception/UnauthorizedView.vue @@ -1,33 +1,7 @@ <template> - <body class="bg-dark text-white py-5"> - <div class="container py-5"> - <div class="row"> - <div class="col-md-2 text-center"> - <p><img src="../../assets/icons/danger.svg" alt="fare"> <br/>Statuskode: 403</p> - </div> - <div class="col-md-10"> - <h3>OOPS!!! Beklager...</h3> - <p>Beklager, din tilgang er nektet av sikkerhetsgrunner pÃ¥ serveren vÃ¥r og ogsÃ¥ vÃ¥re sensitive data.<br/>Vennligst gÃ¥ tilbake til startsiden for Ã¥ fortsette Ã¥ surfe.</p> - <BaseButton :button-text="'Ta meg hjem'" @click="home" /> - </div> - </div> - </div> - </body> + <UnauthorizedPage/> </template> - <script setup lang="ts"> -import { useRouter } from 'vue-router'; -import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue'; - -const router = useRouter(); - -const home = () => { - router.push('/'); -}; -</script> - - -<style scoped> - -</style> \ No newline at end of file + import UnauthorizedPage from '@/components/Exceptions/UnauthorizedPage.vue'; +</script> \ No newline at end of file diff --git a/src/views/Leaderboard/LeaderboardView.vue b/src/views/Leaderboard/LeaderboardView.vue index 07f88f352b3f757e9683b273cdeb8938ab923284..a57d059cbe3584e5192bc907c0cf2ed614331782 100644 --- a/src/views/Leaderboard/LeaderboardView.vue +++ b/src/views/Leaderboard/LeaderboardView.vue @@ -1,209 +1,7 @@ <template> - <br> - <div id="dropdownContainer"> - <h1 class="box">Poengtavle</h1> - </div> - <div id="content"> - <div id="dropdownContainer"> - <div class="box"> - <div class="btn-group-vertical" id="radioContainer" role="group" - aria-label="Vertikal radio knappgruppe"> - <input type="radio" class="btn-check" name="vbtn-radio" id="vbtn-radio1" autocomplete="off" checked> - <label class="btn btn-outline-primary" for="vbtn-radio1" @click="global"><img src="../../assets/globe.png" style="width: 60px" alt="globus"> Global</label> - <input type="radio" class="btn-check" name="vbtn-radio" id="vbtn-radio2" autocomplete="off"> - <label data-cy="friends-leaderboard-btn" class="btn btn-outline-primary" - for="vbtn-radio2" - @click="friends"><img src="../../assets/friends.png" style="width: 60px" alt="venner"> Venner</label> - </div> - </div> - </div> - <main> - <div id="leaderboard"> - <h1><img src="../../assets/items/pigcoin.png" style="width: 2rem" alt="pig coin"> Totale poeng</h1> - <Leaderboard data-cy="total-points-board" :leaderboard="pointsLeaderboardData" - :leaderboardExtra="pointsLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> - </div> - <div id="leaderboard"> - <h1><img src="../../assets/icons/fire.png" style="width: 2rem" alt="ild"> NÃ¥værende rekke</h1> - <Leaderboard data-cy="current-points-board" :leaderboard="currentLeaderboardData" - :leaderboardExtra="currentLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> - </div> - <div id="leaderboard"> - <h1><img src="../../assets/icons/fire.png" style="width: 2rem" alt="ild"> Høyeste rekke</h1> - <Leaderboard data-cy="streak-board" :leaderboard="streakLeaderboardData" - :leaderboardExtra="streakLeaderboardDataExtra" @navigateToUserProfile="navigateToUserProfile" /> - </div> - </main> - </div> - <div id="communityContainer"> - <h1>Totale poeng opptjent som et fellesskap</h1> - <h2>1000000 <img src="../../assets/items/v-buck.png" style="width: 2rem" alt="alt"></h2> - </div> + <ItemShop/> </template> - <script setup lang="ts"> -import { onMounted, ref } from 'vue'; -import { useRouter } from 'vue-router'; -import Leaderboard from '@/components/Leaderboard/LeaderboardTable.vue'; -import { LeaderboardService } from '@/api'; - -let streakLeaderboardData = ref([] as any); -let currentLeaderboardData = ref([] as any); -let pointsLeaderboardData = ref([] as any); - -let streakLeaderboardDataExtra = ref([] as any); -let currentLeaderboardDataExtra = ref([] as any); -let pointsLeaderboardDataExtra = ref([] as any); - -const router = useRouter(); - -async function fetchQuizData() { - await global(); -} - -onMounted(() => { - fetchQuizData(); -}); - -async function global() { - let globalPoints = await LeaderboardService.getLeaderboard({ - type: "TOTAL_POINTS", - filter: "GLOBAL", - }); - let globalStreak = await LeaderboardService.getLeaderboard({ - type: "TOP_STREAK", - filter: "GLOBAL", - }); - let globalCurrentStreak = await LeaderboardService.getLeaderboard({ - type: "CURRENT_STREAK", - filter: "GLOBAL", - }); - let globalPointsYou = await LeaderboardService.getSurrounding({ - type: "TOTAL_POINTS", - filter: "GLOBAL", - entryCount: 2, - }); - let globalStreakYou = await LeaderboardService.getSurrounding({ - type: "TOP_STREAK", - filter: "GLOBAL", - entryCount: 2, - }); - let globalCurrentStreakYou = await LeaderboardService.getSurrounding({ - type: "CURRENT_STREAK", - filter: "GLOBAL", - entryCount: 2, - }); - - pointsLeaderboardData.value = globalPoints.entries; - currentLeaderboardData.value = globalCurrentStreak.entries; - streakLeaderboardData.value = globalStreak.entries; - - pointsLeaderboardDataExtra.value = globalPointsYou.entries; - currentLeaderboardDataExtra.value = globalCurrentStreakYou.entries; - streakLeaderboardDataExtra.value = globalStreakYou.entries; -} - -async function friends() { - let friendsPoints = await LeaderboardService.getLeaderboard({ - type: "TOTAL_POINTS", - filter: "FRIENDS", - }); - let friendsStreak = await LeaderboardService.getLeaderboard({ - type: "TOP_STREAK", - filter: "FRIENDS", - }); - let friendsCurrentStreak = await LeaderboardService.getLeaderboard({ - type: "CURRENT_STREAK", - filter: "FRIENDS", - }); - let friendsPointsYou = await LeaderboardService.getSurrounding({ - type: "TOTAL_POINTS", - filter: "FRIENDS", - entryCount: 2, - }); - let friendsStreakYou = await LeaderboardService.getSurrounding({ - type: "TOP_STREAK", - filter: "FRIENDS", - entryCount: 2, - }); - let friendsCurrentStreakYou = await LeaderboardService.getSurrounding({ - type: "CURRENT_STREAK", - filter: "FRIENDS", - entryCount: 2, - }); - - - pointsLeaderboardData.value = friendsPoints.entries; - currentLeaderboardData.value = friendsCurrentStreak.entries; - streakLeaderboardData.value = friendsStreak.entries; - - pointsLeaderboardDataExtra.value = friendsPointsYou.entries; - currentLeaderboardDataExtra.value = friendsStreakYou.entries; - streakLeaderboardDataExtra.value = friendsCurrentStreakYou.entries; -} - -const navigateToUserProfile = (userId: number) => { - router.push({ name: 'user', params: { id: userId } }); -}; -</script> - -<style scoped> -main { - margin-bottom: 4rem; - width: 80%; - display: flex; - justify-content: space-around; - align-items: start; - flex-wrap: wrap; - flex-direction: row; -} - -#leaderboard { - width: 400px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - margin-bottom: 3rem; -} - -#content { - display: flex; - flex-direction: row; - justify-content: center; - flex-wrap: wrap; -} - -.box { - width: 90%; -} - -h1 { - font-weight: 700; - margin-bottom: 1rem; -} - -#dropdownContainer { - display: flex; - justify-content: center; - margin-bottom: 2rem; -} - -#radioContainer { - display: flex; - justify-content: center; - margin-bottom: 2rem; - width: 100%; - margin-top: 3.6rem; -} - -#communityContainer { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - margin-bottom: 5rem; -} - -</style> \ No newline at end of file + import ItemShop from '@/components/Leaderboard/LeaderboardRank.vue'; +</script> \ No newline at end of file diff --git a/src/views/Shop/ShopView.vue b/src/views/Shop/ShopView.vue index 15f17c636da19d0e18deb7e1f627027ff310a7ac..667f6802ec2e019af1144e975e9924c542dd3c61 100644 --- a/src/views/Shop/ShopView.vue +++ b/src/views/Shop/ShopView.vue @@ -1,171 +1,7 @@ <template> - <div id="background"> - <br> - <div id="dropdownContainer"> - <h1 class="box">Butikk</h1> - </div> - <div class="container d-flex justify-content-center"> - <div class="row col-md-10"> - <div class="col-md-12"> - <h1>Stash</h1> - <div class="category row justify-content-between mb-5 m-2"> - <div class="card text-center" style="width: 16rem; border: none"> - <img src="../../assets/items/adfree.png" class="card-img-top" alt="..."> - <div class="card-body"> - <h5 class="card-title">Adfree</h5> - <button type="button" class="btn btn-primary" id="buttonStyle" @click="buyNoAds"> - +35kr</button> - </div> - </div> - <div class="card text-center" style="width: 16rem; border: none"> - <img src="../../assets/items/piggybank.webp" class="card-img-top" alt="..."> - <div class="card-body"> - <h5 class="card-title">Premium</h5> - <button type="button" class="btn btn-primary" id="buttonStyle" - @click="buyPremium">+50kr</button> - </div> - </div> - </div> - </div> - <div class="col-md-12"> - <h1>Items</h1> - <div class="category row justify-content-between mb-5 m-2"> - <div v-for="product in products" :key="product.id" class="card text-center" - style="width: 16rem; border: none"> - <img :src="`http://localhost:8080/api/images/${product.imageId}`" class="card-img-top" - alt="..." /> - <div class="card-body"> - <h5 class="card-title">{{ product.itemName }}</h5> - <ShopButton v-if="!product.alreadyBought" :button-text="product.price" - @click="buyItem(product.id)"></ShopButton> - <p v-else>Owned</p> - </div> - </div> - </div> - </div> - <div class="col-md-12"> - <h1>Stash</h1> - <div class="category row justify-content-between mb-5 m-2"> - <div class="card text-center" style="width: 16rem; border: none"> - <img src="../../assets/items/coffee.jpg" class="card-img-top" alt="..."> - <div class="card-body"> - <h5 class="card-title">Free Coffee</h5> - <ShopButton button-text="500"></ShopButton> - </div> - </div> - <div class="card text-center" style="width: 16rem; border: none"> - <img src="../../assets/items/viaplay.jpg" class="card-img-top" alt="..."> - <div class="card-body"> - <h5 class="card-title">1 Month Viaplay</h5> - <ShopButton button-text="10000"></ShopButton> - </div> - </div> - <div class="card text-center" style="width: 16rem; border: none"> - <img src="../../assets/items/pirbad.png" class="card-img-top" alt="..."> - <div class="card-body"> - <h5 class="card-title">-10% rabatt</h5> - <ShopButton button-text="1000"></ShopButton> - </div> - </div> - </div> - </div> - - </div> - </div> - </div> + <ItemShop/> </template> <script setup lang="ts"> -import ShopButton from '@/components/Shop/ShopButton.vue'; -import { ref, onMounted } from 'vue'; -import { UserService } from '@/api'; -import { useUserInfoStore } from '@/stores/UserStore'; -import { ItemService } from '@/api'; - -const products = ref([] as any); - -const getStore = async () => { - const response = await ItemService.getStore(); - products.value = response; - console.log(response); -} - -const buyItem = async (itemId: number) => { - try { - const response = await ItemService.buyItem({ itemId: itemId }); - console.log(response); - const responseStore = await ItemService.getStore(); - products.value = responseStore; - } catch (error) { - console.log(error); - } -} - -const buyPremium = async () => { - try { - const response = await UserService.updateSubscriptionLevel({ subscriptionLevel: 'PREMIUM' }); - useUserInfoStore().setUserInfo({ - subscriptionLevel: 'PREMIUM', - }) - } catch (error) { - console.log(error); - } -} - -const buyNoAds = async () => { - try { - const response = await UserService.updateSubscriptionLevel({ subscriptionLevel: 'NO_ADS' }); - useUserInfoStore().setUserInfo({ - subscriptionLevel: 'NO_ADS', - }) - } catch (error) { - console.log(error); - } -} - -onMounted(() => { - getStore(); -}) -</script> - -<style scoped> -.card { - box-shadow: none; - margin: 10px; - border-radius: 8px; - padding-left: 5px; - padding-right: 5px; - /* Rounded corners */ -} - -.box { - width: 90%; - justify-content: center; - text-align: center; - font-size: 5rem; - font-weight: 700; -} - -.card:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.card-body { - height: 100px; - padding: 5px; -} - -.col-md-12 { - border-bottom: 2px solid #000000; -} - -#dropdownContainer { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 2rem; -} - -#background { -} -</style> \ No newline at end of file + import ItemShop from '@/components/Shop/ItemShop.vue'; +</script> \ No newline at end of file diff --git a/src/views/User/UserFriendsView.vue b/src/views/User/UserFriendsView.vue index 1858195a3e056672c5f1f13120c03358b626b8e4..075b06aeae3091f5fa45091845882a2c6cd59225 100644 --- a/src/views/User/UserFriendsView.vue +++ b/src/views/User/UserFriendsView.vue @@ -1,605 +1,7 @@ <template> - <div class="container" style="margin-bottom: 3rem;"> - <h1 class="my-3">Dine venner</h1> - <div> - <button class="btn pull-right" @click="addNewFriends" id="addFriend">+ Legg til venn</button> - <div class="my-3"> - <button class="btn pages" @click="setupFriends" :class="{ 'active-tab': showFriends }"> - Dine venner - </button> - <button class="btn pages" @click="requestFriend" :class="{ 'active-tab': showRequests }"> - Venneforespørsler - </button> - </div> - </div> - <div v-if="showFriends"> - <div v-if="elementsInFriends"> - <div class="row"> - <div class="col-lg-3" v-for="friend in friends" :key="friend.id"> - <div class="card card-one"> - <div class="header"> - <div v-if="friend.profileImage" class="avatar"> - <img :src="'http://localhost:8080/api/images/' + friend.profileImage" alt=""> - </div> - <div v-else class="avatar"> - <img :src="'../src/assets/userprofile.png'" alt=""> - </div> - </div> - <h3><router-link to="" data-cy="navigateToFriend" href="#" class="btn stretched-link" - id="profileName" @click="navigateToFriend(friend.id)">{{ - friend.firstName }} {{ friend.lastName }}</router-link></h3> - <div class="desc">{{ friend.firstName }} {{ friend.lastName }}</div> - <div class="contacts"> - <a class="text removeFriend" data-bs-toggle="collapse" - :href="'#collapseExample' + friend.id" role="button" aria-expanded="false" - :aria-controls="'collapseExample' + friend.id"> - Se mer - </a> - <div class="collapse" :id="'collapseExample' + friend.id"> - <button class="btn btn-danger" @click="removeFriend(friend.id)"> - <h5><img src="@/assets/icons/remove-white.svg" style="width: 30px"> Fjern venn - </h5> - </button> - </div> - </div> - </div> - </div> - </div> - </div> - <div v-else>Ingen venner</div> - </div> - <div v-else-if="showRequests" class="row"> - <div class="content-body"> - <div v-if="elementsInFriendRequest" id="requests"> - <div class="request" v-for="(friend) in friendRequests" :key="friend.id"> - <div v-if="friend.profileImage !== null"><img id="profilePicture" - :src="'http://localhost:8080/api/images/' + friend.profileImage" alt="bruker" - class="profile-photo-lg"></div> - <div v-else><img id="profilePicture" :src="'../src/assets/userprofile.png'" alt="bruker" - class="profile-photo-lg"></div> - <h2>{{ friend.firstName }}</h2> - <button class="btn btn-success mx-2" - @click="acceptRequest(friend.id)">Godta</button> - <button class="btn btn-danger" @click="rejectRequest(friend.id)">AvslÃ¥</button> - </div> - </div> - <div v-else>Ingen venneforespørsler</div> - </div> - </div> - <div v-if="showAddFriend" class="modal" tabindex="-1" role="dialog" - style="display:block; background-color: rgba(0,0,0,0.5);"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title">Legg til venn</h5> - <button type="button" class="close" @click="showAddFriend = false"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="modal-body d-flex justify-content-center align-items-center flex-column"> - <form class="col-md-10 d-flex justify-content-center align-items-center flex-row my-4" - id="searchBox" role="search" @submit.prevent="searchProfile(searchWord)"> - <input class="form-control me-2 custom-border" type="search" placeholder="Søk" - aria-label="Søk" v-model="searchWord"> - <button class="btn btn-success" type="submit">Søk</button> - </form> - <div class="col-md-12"> - <div class="people-nearby"> - <div v-for="user in searchedUsers" :key="user.id" class="nearby-user"> - <div class="row d-flex align-items-center"> - <div class="col-md-2 col-sm-2"> - <div v-if="user.profileImage !== null"><img id="profilePicture" - :src="'http://localhost:8080/api/images/' + user.profileImage" - alt="bruker" class="profile-photo-lg"></div> - <div v-else><img id="profilePicture" :src="'../src/assets/userprofile.png'" - alt="bruker" class="profile-photo-lg"></div> - </div> - <div class="col-md-7 col-sm-7"> - <h5><a href="#" class="profile-link" @click="toUserProfile(user.id)">{{ - user.firstName }}</a> - </h5> - </div> - <div class="col-md-3 col-sm-3"> - <button class="btn btn-primary pull-right" @click="addFriend(user.id)" - :disabled="friendRequestsSent[user.id]" - v-if="!friendRequestsSent[user.id]">Legg til venn</button> - <button class="btn btn-secondary pull-right" disabled - v-if="friendRequestsSent[user.id]">Forespørsel sendt</button> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </div> + <UserFriends/> </template> - - - <script setup lang="ts"> -import { type Ref, ref, onMounted } from 'vue'; -import { useRouter } from 'vue-router'; -import { FriendService, UserService } from '@/api'; -import type { UserDTO } from '@/api'; - -const router = useRouter(); -const friends = ref(); -const showFriends = ref(true); -const showRequests = ref(false); -const showAddFriend = ref(false); -const friendRequests = ref([] as any); -const addFriends = ref([] as any); -const searchedUsers = ref([] as any); - -const friendRequestsSent: Ref<Record<number, boolean>> = ref({}); - -const searchWord = ref(""); - -const elementsInFriendRequest = ref(false); -const elementsInFriends = ref(false); - -const toUserProfile = (userId: number) => { - router.push('/profile/' + userId); -}; - -const searchProfile = async (searchTerm: string) => { - const userPayload = { - searchTerm: searchTerm as string, - filter: 'NON_FRIENDS' as string, - }; - try { - const response = await UserService.getUsersByNameAndFilter(userPayload); - searchedUsers.value = response; - console.log(response); - } catch (error) { - console.error('Failed to search for profile', error); - } -}; - -const addNewFriends = async () => { - const userPayload = { - amount: 6 as number, - filter: 'NON_FRIENDS' as string, - }; - try { - const response = await UserService.getRandomUsers(userPayload); - searchedUsers.value = response; - showAddFriend.value = true; - } catch (error) { - console.error('Failed to add friend', error); - } -}; - -async function addFriend(friendID: number) { - try { - await FriendService.addFriendRequest({ userId: friendID }); - // Use a spread to update the state and keep immutability - friendRequestsSent.value = { ...friendRequestsSent.value, [friendID]: true }; - } catch (error) { - console.error('Failed to send friend request', error); - } -} - -async function requestFriend() { - showRequests.value = true; - showFriends.value = false; - try { - const response = await FriendService.getFriendRequests(); - friendRequests.value = response; - elementsInFriendRequest.value = response.length > 0; - console.log("Friend requests: " + response); - } catch (error) { - console.error('Failed to fetch friend requests', error); - } -} - -const navigateToFriend = (friendID: number) => { - router.push('/profile/' + friendID); -}; - -const removeFriend = async (friendID: number) => { - try { - await FriendService.deleteFriendOrFriendRequest({ friendId: friendID }); - const responseFriends = await FriendService.getFriends(); - friends.value = responseFriends; - } catch (error) { - console.error('Failed to remove friend', error); - } -}; - -const setupFriends = async () => { - showFriends.value = true; - showRequests.value = false; - try { - const response = await FriendService.getFriends(); - friends.value = response; - elementsInFriends.value = response.length > 0; - console.log(response); - } catch (error) { - console.error('Failed to fetch friends', error); - } -}; - -const acceptRequest = async (requestID: number) => { - try { - await FriendService.acceptFriendRequest({ friendId: requestID }); - const responseRequest = await FriendService.getFriendRequests(); - friendRequests.value = responseRequest; - const responseFriends = await FriendService.getFriends(); - friends.value = responseFriends; - } catch (error) { - console.error('Failed to accept friend request', error); - } -}; - -const rejectRequest = async (requestID: number) => { - try { - await FriendService.deleteFriendOrFriendRequest({ friendId: requestID }); - const response = await FriendService.getFriendRequests(); - friendRequests.value = response; - } catch (error) { - console.error('Failed to reject friend request', error); - } -}; - -onMounted(() => { - setupFriends(); -}); -</script> - - -<style scoped> -body { - background-color: #f0f6ff; - color: #28384d; - -} - -/*social */ -.card-one { - position: relative; - width: 300px; - background: #fff; - box-shadow: 0 10px 7px -5px rgba(0, 0, 0, 0.4); -} - -.card { - margin-bottom: 35px; - padding-bottom: 1rem; - box-shadow: 0 10px 20px 0 rgba(26, 44, 57, 0.14); - border: none; -} - -.follower-wrapper li { - list-style-type: none; - color: #fff; - display: inline-block; - float: left; - margin-right: 20px; -} - -.social-profile { - color: #fff; -} - -.social-profile a { - color: #fff; -} - -.social-profile { - position: relative; - margin-bottom: 150px; -} - -.social-profile .user-profile { - position: absolute; - bottom: -75px; - width: 150px; - height: 150px; - border-radius: 50%; - left: 50px; -} - -.social-nav { - position: absolute; - bottom: 0; -} - -.social-prof { - color: #333; - text-align: center; -} - -.social-prof .wrapper { - width: 70%; - margin: auto; - margin-top: -100px; -} - -.social-prof img { - width: 150px; - height: 150px; - border-radius: 50%; - margin-bottom: 20px; - border: 5px solid #fff; - /*border: 10px solid #70b5e6ee;*/ -} - -.social-prof h3 { - font-size: 36px; - font-weight: 700; - margin-bottom: 0; -} - -.social-prof p { - font-size: 18px; -} - -.social-prof .nav-tabs { - border: none; -} - -.card .nav>li { - position: relative; - display: block; -} - -.card .nav>li>a { - position: relative; - display: block; - padding: 10px 15px; - font-weight: 300; - border-radius: 4px; -} - -.card .nav>li>a:focus, -.card .nav>li>a:hover { - text-decoration: none; - background-color: #eee; -} - -.card .s-nav>li>a.active { - text-decoration: none; - background-color: #3afe; - color: #fff; -} - -.text-blue { - color: #3afe; -} - -ul.friend-list { - margin: 0; - padding: 0; -} - -ul.friend-list li { - list-style-type: none; - display: flex; - align-items: center; -} - -ul.friend-list li:hover { - background: rgba(0, 0, 0, .1); - cursor: pointer; -} - -ul.friend-list .left img { - width: 45px; - height: 45px; - border-radius: 50%; - margin-right: 20px; -} - -ul.friend-list li { - padding: 10px; -} - -ul.friend-list .right h3 { - font-size: 16px; - font-weight: 700; - margin-bottom: 0; -} - -ul.friend-list .right p { - font-size: 11px; - color: #6c757d; - margin: 0; -} - -.social-timeline-card .dropdown-toggle::after { - display: none; -} - -.info-card h4 { - font-size: 15px; -} - -.info-card h2 { - font-size: 18px; - margin-bottom: 20px; -} - -.social-about .social-info { - font-size: 16px; - margin-bottom: 20px; -} - -.social-about p { - margin-bottom: 20px; -} - -.info-card i { - color: #3afe; -} - -.card-one { - position: relative; - width: 300px; - background: #fff; - box-shadow: 0 10px 7px -5px rgba(0, 0, 0, 0.4); -} - -.card-one .header { - position: relative; - width: 100%; - height: 60px; - background-color: rgba(7, 46, 74, 0.895); -} - -.card-one .header::before, -.card-one .header::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: inherit; -} - -.card-one .header::before { - -webkit-transform: skewY(-8deg); - transform: skewY(-8deg); - -webkit-transform-origin: 100% 100%; - transform-origin: 100% 100%; -} - -.card-one .header::after { - -webkit-transform: skewY(8deg); - transform: skewY(8deg); - -webkit-transform-origin: 0 100%; - transform-origin: 0 100%; -} - -.card-one .header .avatar { - position: absolute; - left: 50%; - top: 30px; - margin-left: -50px; - z-index: 5; - width: 100px; - height: 100px; - border-radius: 50%; - overflow: hidden; - background: #ccc; - border: 3px solid #fff; -} - -.card-one .header .avatar img { - position: absolute; - top: 50%; - left: 50%; - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - width: 100px; - height: auto; -} - -.card-one h3 { - position: relative; - margin: 80px 0 30px; - text-align: center; -} - -.card-one h3::after { - content: ''; - position: absolute; - bottom: -15px; - left: 50%; - margin-left: -15px; - width: 30px; - height: 1px; - background: #000; -} - -.card-one .desc { - padding: 0 1rem 2rem; - text-align: center; - line-height: 1.5; - color: #777; -} - -#gallery li { - width: 24%; - float: left; - margin: 6px; - -} - -.removeFriend { - text-wrap: nowrap; -} - -.contacts { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -#profileName { - font-size: 1.5rem; - font-weight: 600; - width: 100%; -} - -#requests { - display: flex; - flex-direction: column; - align-items: center; -} - -.request { - display: flex; - justify-content: center; - align-items: center; - margin: 1rem; -} - -#profilePicture { - width: 70px; - height: 70px; - border-radius: 50%; - margin-right: 1rem; - border: 2px solid #000; -} - -.modal-content { - padding: 1rem; -} - -.modal-header { - margin-bottom: 5px; -} - -.pages { - border-bottom: 1px solid #000; - border-radius: 0px; - margin: 0px 5px; -} - -.pages { - border-bottom: 2px solid #000; - /* default border */ - border-radius: 0px; - margin: 0px 5px; -} - -.active-tab { - border-bottom: 4px solid #000; - /* thicker border when active */ -} - -#addFriend { - background-color: #084766; - color: white; -} - -#addFriend:hover { - background-color: #003b58f5; -} -</style> \ No newline at end of file + import UserFriends from '@/components/Friends/UserFriends.vue'; +</script> \ No newline at end of file