diff --git a/src/assets/icons/template.svg b/src/assets/icons/template.svg new file mode 100644 index 0000000000000000000000000000000000000000..a1b6aed6090f0a233c6f5361b99dee7cc93fab01 --- /dev/null +++ b/src/assets/icons/template.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m438-240 226-226-58-58-169 169-84-84-57 57 142 142ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg> \ No newline at end of file diff --git a/src/components/Admin/AdminFeedback.vue b/src/components/Admin/AdminFeedback.vue new file mode 100644 index 0000000000000000000000000000000000000000..677b77ae69a4d443df6d46ec1722d910284163fc --- /dev/null +++ b/src/components/Admin/AdminFeedback.vue @@ -0,0 +1,73 @@ +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import { type FeedbackResponseDTO, UserService } from '@/api' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' + +const feedbacks = ref<FeedbackResponseDTO[]>([]); + +onMounted(async () => { + try { + feedbacks.value = await UserService.getFeedback(); + console.log(feedbacks.value) + } catch (error) { + handleUnknownError(error); + } +}) + +const formattedDate = (dateStr?: string): string => { + if (!dateStr) return ''; + return new Date(dateStr).toLocaleString(); +}; +</script> + +<template> + <div class="feedback-container"> + <h1>Feedback List</h1> + <div class="feedback-list"> + <!-- Loop through feedback items --> + <div class="feedback-item" v-for="feedback in feedbacks" :key="feedback.id"> + <div class="email">{{ feedback.email }}</div> + <div class="message">{{ feedback.message }}</div> + <div class="created-at">{{ formattedDate(feedback.createdAt) }}</div> + </div> + </div> + </div> +</template> + +<style scoped> +.feedback-container { + max-width: 800px; + margin: auto; + padding: 20px; +} + +.feedback-list { + margin-top: 20px; + border-top: 1px solid #ccc; +} + +.feedback-item { + padding: 10px; + border-bottom: 1px solid #ccc; +} + +.email, .message, .created-at { + padding: 5px 0; +} + +.email { + font-weight: bold; + color: #333; +} + +.message { + margin: 5px 0; + line-height: 1.5; + color: #666; +} + +.created-at { + font-size: 0.8rem; + color: #999; +} +</style> \ No newline at end of file diff --git a/src/components/BaseComponents/NavBar.vue b/src/components/BaseComponents/NavBar.vue index 8163476d622b68468a517dd35151797087334aaf..e6c1827cec24f450e618b9f1a648f6a0f8dd4149 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -45,7 +45,7 @@ <img src="@/assets/icons/storefront.svg">Butikk </router-link> </li> - <li class="nav-item dropdown"> + <li class="nav-item dropdown d-flex flex-column"> <a data-mdb-dropdown-init class=" nav-link dropdown-toggle hidden-arrow notification" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <img src="/src/assets/icons/bell-white.svg"> @@ -60,7 +60,7 @@ <img :src="notificationImageMapper[String(item.notificationType)]" alt="Varslingsikon" class="notification-icon"> </div> <div class="flex-grow-1 ms-3"> - <div class="not-item dropdown-item">{{item.message}}</div> + <div class="not-item dropdown-item" id="notificationText">{{item.message}}</div> </div> </router-link> </li> @@ -69,7 +69,7 @@ <li>Ingen varslinger</li> </ul> </li> - <li v-if="userStore.isLoggedIn" class="nav-item dropdown"> + <li v-if="userStore.isLoggedIn" class="nav-item dropdown d-flex flex-column"> <a data-cy="user" :class="['nav-link', 'dropdown-toggle', 'username-text', 'text-white', { 'underline-active': !isAnyActivePage() }]" @@ -122,7 +122,7 @@ <li v-if="useUserInfoStore().role === 'ADMIN'"> <router-link data-cy="admin" class="dropdown-item dropdown-username-link" - :to="toSetting()" + :to="toAdmin()" exact-active-class="active-link" @click="toggleDropdown"> <img src="@/assets/icons/admin.svg">Admin @@ -313,6 +313,15 @@ function toFeedback(): string { return '/feedback'; } +/** + * Redirects to the admin page. + * + * @returns {string} The URL for the admin page. + */ +function toAdmin(): string { + return '/admin'; +} + /** * Redirects to the friends page. * @@ -463,6 +472,12 @@ onMounted(() => { margin: 0 140px; } +@media (max-width: 768px) { + .container-fluid { + margin: 0 20px; + } +} + #logo { font-size: 2.5rem; height: 100%; @@ -485,5 +500,14 @@ onMounted(() => { display: none; } +#notificationText { + text-wrap: nowrap; +} + +@media (max-width: 768px) { + #notificationText { + text-wrap: wrap; + } +} </style> \ No newline at end of file diff --git a/src/components/Friends/UserFriends.vue b/src/components/Friends/UserFriends.vue index d83a13ef9e201ef83adafbc15810d385abae0b1c..9767d8eb429321ef79cc608902d28a59b0d43888 100644 --- a/src/components/Friends/UserFriends.vue +++ b/src/components/Friends/UserFriends.vue @@ -15,7 +15,7 @@ <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="friendBox d-flex flex-wrap" v-for="friend in friends" :key="friend.id"> <div class="card card-one"> <div class="header"> <div v-if="friend.profileImage" class="avatar"> @@ -27,7 +27,7 @@ </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> + 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" @@ -71,7 +71,8 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">Legg til venn</h5> - <button type="button" class="close btn-close" @click="showAddFriend = false" aria-label="Close"></button> + <button type="button" class="close btn-close" @click="showAddFriend = false" + aria-label="Close"></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" @@ -86,8 +87,8 @@ <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="apiUrl + '/api/images/' + user.profileImage" - alt="bruker" class="profile-photo-lg"></div> + :src="apiUrl + '/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> @@ -319,7 +320,7 @@ body { /*social */ .card-one { position: relative; - width: 300px; + width: 200px; background: #fff; box-shadow: 0 10px 7px -5px rgba(0, 0, 0, 0.4); } @@ -660,4 +661,8 @@ ul.friend-list .right p { #addFriend:hover { background-color: #003b58f5; } + +.friendBox { + width: 250px; +} </style> \ No newline at end of file diff --git a/src/components/Login/LoginParent.vue b/src/components/Login/LoginParent.vue index bacabbcef217d37c525a4ae92d8e06aff2e2e23f..c1735b01d3b28aad80150f6c5e49f1b3a10fb5a1 100644 --- a/src/components/Login/LoginParent.vue +++ b/src/components/Login/LoginParent.vue @@ -31,6 +31,12 @@ import LoginForm from '@/components/Login/LoginForm.vue' box-shadow: rgba(57, 57, 63, 0.5) 0px 1px 20px 0px; } + @media (max-width: 600px){ + .box { + padding: 0; + } + } + .title { font-size: 60px; color: white; diff --git a/src/components/Settings/SettingsBank.vue b/src/components/Settings/SettingsBank.vue index c923b7005efd5b4286edd9a4b26fe11954cb8f30..ba2f237634f03bd85f77aeae8342d0a4971d9c77 100644 --- a/src/components/Settings/SettingsBank.vue +++ b/src/components/Settings/SettingsBank.vue @@ -52,7 +52,7 @@ <script setup lang="ts"> import { ref, onMounted } from 'vue'; import BaseInput from '@/components/BaseComponents/Input/BaseInput.vue'; -import type { UserUpdateDTO } from '@/api' +import { AccountControllerService, type BalanceDTO, BankProfileControllerService, type UserUpdateDTO } from '@/api' import { UserService } from '@/api'; import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' @@ -128,13 +128,16 @@ async function getAccountInfo() { try { let response = await UserService.getUser(); savingsAccount.value = response.savingsAccountBBAN; - /*if (response.savingsAccount?.balance) { - savingsAccountBalance.value = response.savingsAccount?.balance - }*/ + let bban1: any = response.savingsAccountBBAN; + AccountControllerService.getAccountsByBban({bban: bban1}).then((balance: BalanceDTO) => { + savingsAccountBalance.value = balance.balance; + }) + spendingAccount.value = response.checkingAccountBBAN; - /*if (response.checkingAccount?.balance) { - spendingAccountBalance.value = response.checkingAccountBBAN?.balance - }*/ + let bban2: any = response.checkingAccountBBAN; + AccountControllerService.getAccountsByBban({bban: bban2}).then((balance: BalanceDTO) => { + spendingAccountBalance.value = balance.balance; + }) } catch (err) { handleUnknownError(err) } diff --git a/src/components/Settings/SettingsProfile.vue b/src/components/Settings/SettingsProfile.vue index a8575a144348ceb12e0d926f6faf528344424f7c..8080512c78edb0b955407fa50a79f25e988821c7 100644 --- a/src/components/Settings/SettingsProfile.vue +++ b/src/components/Settings/SettingsProfile.vue @@ -19,6 +19,8 @@ let banners = ref([] as any) let hasBanners = ref(false); let selectedBannerId = ref(0); const selectedBanner = ref() +const errorMsg = ref(''); +const successMsg = ref(''); const iconSrc = ref('../src/assets/userprofile.png'); const fileInputRef = ref(); @@ -152,13 +154,16 @@ const handleSubmit = async () => { lastName: surnameRef.value, }; try { - UserService.update({ requestBody: updateUserPayload }) + await UserService.update({ requestBody: updateUserPayload }) useUserInfoStore().setUserInfo({ firstname: firstNameRef.value, lastname: surnameRef.value, }) + errorMsg.value = ''; + successMsg.value = 'Profilen ble oppdatert!'; } catch (err) { - handleUnknownError(err); + errorMsg.value = handleUnknownError(err); + successMsg.value = ''; console.error(err) } } @@ -174,40 +179,51 @@ onMounted(() => { <div class="tab-pane active" id="profile"> <h6>DIN PROFILINFORMASJON</h6> <hr> - <form @submit.prevent="handleSubmit" novalidate> - <div class="user-avatar"> - <input type="file" ref="fileInputRef" @change="handleFileChange" accept=".jpg, .jpeg, .png" - style="display: none" /> - <img :src="iconSrc" alt="Brukeravatar" style="width: 200px; height: 200px;"> - <div class="mt-2"> - <button type="button" class="btn btn-primary classyButton" @click="triggerFileUpload"><img - src="../../assets/icons/download.svg"> Last opp bilde</button> + <form @submit.prevent="handleSubmit" novalidate class="d-flex infoHolder"> + <div> + <div class="user-avatar"> + <input type="file" ref="fileInputRef" @change="handleFileChange" accept=".jpg, .jpeg, .png" + style="display: none" /> + <img :src="iconSrc" alt="Brukeravatar" style="width: 200px; height: 200px;"> + <div class="mt-2"> + <button type="button" class="btn btn-primary classyButton" @click="triggerFileUpload"><img + src="../../assets/icons/download.svg"> Last opp bilde</button> + </div> </div> </div> - <div class="form-group"> - <BaseInput data-cy="first-name" :model-value="firstNameRef" @input-change-event="handleFirstNameInputEvent" - id="firstNameInputChange" input-id="first-name-new" type="text" label="Fornavn" - placeholder="Skriv inn ditt fornavn" invalid-message="Vennligst skriv inn ditt fornavn" - style="max-width: 300px" /> - </div> - <br> - <div class="form-group"> - <BaseInput data-cy="last-name" :model-value="surnameRef" @input-change-event="handleSurnameInputEvent" - id="surnameInput-change" input-id="surname-new" type="text" label="Etternavn" - placeholder="Skriv inn ditt etternavn" invalid-message="Vennligst skriv inn ditt etternavn" - style="max-width: 300px" /> + <div class="mx-5"> + <div class="form-group"> + <BaseInput data-cy="first-name" :model-value="firstNameRef" @input-change-event="handleFirstNameInputEvent" + id="firstNameInputChange" input-id="first-name-new" type="text" label="Fornavn" + placeholder="Skriv inn ditt fornavn" invalid-message="Vennligst skriv inn ditt fornavn" class="inputDynamic" /> + </div> + <br> + <div class="form-group"> + <BaseInput data-cy="last-name" :model-value="surnameRef" @input-change-event="handleSurnameInputEvent" + id="surnameInput-change" input-id="surname-new" type="text" label="Etternavn" + placeholder="Skriv inn ditt etternavn" invalid-message="Vennligst skriv inn ditt etternavn" + class="inputDynamic"/> + </div> + <br> + <div class="d-flex"> + <p class="text-danger"> {{ errorMsg }}</p> + <p class="text-success"> {{ successMsg }}</p> + </div> + <button data-cy="profile-submit-btn" type="submit" class="btn btn-primary classyButton">Oppdater profil</button> </div> - <br> - <button data-cy="profile-submit-btn" type="submit" class="btn btn-primary classyButton">Oppdater profil</button> </form> <hr> <div> <h6>Banners</h6> <div v-if="hasBanners" class="scrolling-wrapper-badges row flex-row flex-wrap mt-2 pb-2 pt-2"> - <div v-for="banner in banners" :key="banner.id" class="card text-center banner justify-content-center d-flex align-items-center" @click="selectItem(banner.id)" - :class="{ 'selected-banner': banner.id === selectedBannerId }" data-bs-toggle="tooltip" - data-bs-placement="top" data-bs-custom-class="custom-tooltip" :data-bs-title="banner.criteria"> - <img :src="apiUrl + `/api/images/${banner.imageId}`" class="card-img-top" :class="{ 'selected-banner': banner.id === selectedBanner }" alt="Banner" style="width: 200px; height: 100px" @click="selectItem(banner.imageId)" /> + <div v-for="banner in banners" :key="banner.id" + class="card text-center banner justify-content-center d-flex align-items-center" + @click="selectItem(banner.id)" :class="{ 'selected-banner': banner.id === selectedBannerId }" + data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="custom-tooltip" + :data-bs-title="banner.criteria"> + <img :src="apiUrl + `/api/images/${banner.imageId}`" class="card-img-top" + :class="{ 'selected-banner': banner.id === selectedBanner }" alt="Banner" + style="width: 200px; height: 100px" @click="selectItem(banner.imageId)" /> </div> </div> <div v-else> @@ -259,4 +275,26 @@ onMounted(() => { cursor: pointer; width: 200px; } + +.infoHolder { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +@media (max-width: 1252px) { + .infoHolder { + flex-direction: column; + } +} + +.inputDynamic { + width: 340px; +} + +@media (max-width: 960px) { + .inputDynamic { + width: 200px; + } +} </style> \ No newline at end of file diff --git a/src/components/Shop/ItemShop.vue b/src/components/Shop/ItemShop.vue index 38173e0b83a8ab29db3e8ef779262c55cf9f2021..60f544d0c4897cdc076e852ecc38f2a5a629b36b 100644 --- a/src/components/Shop/ItemShop.vue +++ b/src/components/Shop/ItemShop.vue @@ -1,191 +1,240 @@ <template> - <div id="background"> - <br /> - <div id="dropdownContainer"> - <h1 class="box">Butikk</h1> - <div> - <p class="mb-1 h2" data-cy="points">{{ points }}<img src="@/assets/items/pigcoin.png" style="width: 4rem" /></p> - </div> + <div id="background"> + <br /> + <div id="dropdownContainer"> + <h1 class="box">Butikk</h1> + <div> + <p class="mb-1 h2" data-cy="points">{{ points }}<img src="@/assets/items/pigcoin.png" style="width: 4rem" /></p> </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 mb-2 m-2"> - <div class="card text-center justify-content-center align-items-center" style="width: 8rem; border: none"> - <img src="../../assets/items/adfree.png" class="card-img-top" alt="..." style="width: 100px; height: 100px;" /> - <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="container d-flex justify-content-center"> + <div class="row col-md-10"> + <div class="col-md-12"> + <h1>Stash</h1> + <div class="category row mb-2 m-2"> + <div class="card text-center justify-content-center align-items-center" style="width: 8rem; border: none"> + <img src="../../assets/items/adfree.png" class="card-img-top" alt="..." + style="width: 100px; height: 100px;" /> + <div class="card-body"> + <h5 class="card-title">Adfree</h5> + <button type="button" class="btn btn-primary" id="buttonStyle" data-toggle="modal" + data-target="#adfreeModal"> + +35kr + </button> </div> - <div class="card text-center justify-content-center align-items-center" style="width: 8rem; border: none"> - <img src="../../assets/items/piggybank.webp" class="card-img-top" alt="..." style="width: 100px; height: 100px;" /> - <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 class="card text-center justify-content-center align-items-center" style="width: 8rem; border: none"> + <img src="../../assets/items/piggybank.webp" class="card-img-top" alt="..." + style="width: 100px; height: 100px;" /> + <div class="card-body"> + <h5 class="card-title">Premium</h5> + <button type="button" class="btn btn-primary" id="buttonStyle" data-toggle="modal" + data-target="#premiumModal"> + +50kr + </button> </div> </div> </div> - <div class="col-md-12"> - <h1>Items</h1> - <div class="category row mb-2 m-2"> - <div v-for="product in products" :key="product.id" class="card text-center d-flex justify-content-center align-items-center" - style="width: 16rem; border: none"> - <img :src="apiUrl + `/api/images/${product.imageId}`" style="width: 200px; height: 100px;" class="card-img-top" alt="..." /> - <div class="card-body"> - <h5 class="card-title">{{ product.itemName }}</h5> - <h6>{{ product.price }}<img src="../../assets/items/pigcoin.png" style="width: 2rem" /></h6> - <ShopButton - v-if="!product.alreadyBought" - button-text="Buy item" - :disabled="product.price > points" - @click="buyItem(product.id)" - /> - <p v-else>Owned</p> - </div> + </div> + <div class="col-md-12"> + <h1>Items</h1> + <div class="category row mb-2 m-2"> + <div v-for="product in products" :key="product.id" + class="card text-center d-flex justify-content-center align-items-center" + style="width: 16rem; border: none"> + <img :src="apiUrl + `/api/images/${product.imageId}`" style="width: 200px; height: 100px;" + class="card-img-top" alt="..." /> + <div class="card-body"> + <h5 class="card-title">{{ product.itemName }}</h5> + <h6>{{ product.price }}<img src="../../assets/items/pigcoin.png" style="width: 2rem" /></h6> + <ShopButton v-if="!product.alreadyBought" button-text="Buy item" :disabled="product.price > points" + @click="buyItem(product.id)" /> + <p v-else>Owned</p> </div> </div> </div> - <div class="col-md-12"> - <h1>Cool items</h1> - <div class="category row mb-2 m-2"> - <div class="card text-center d-flex justify-content-center align-items-center" style="width: 8rem; border: none"> - <img src="../../assets/items/coffee.jpg" class="card-img-top" alt="..." style="width: 100px; height: 100px;"> - <div class="card-body"> - <h5 class="card-title">Free Coffee</h5> - <h6>500<img src="../../assets/items/pigcoin.png" style="width: 2rem"></h6> - <ShopButton - button-text="Buy item" - :disabled="500 > points" - @click="buySomething()" - /> - </div> - </div> - <div class="card text-center d-flex justify-content-center align-items-center" style="width: 8rem; border: none"> - <img src="../../assets/items/viaplay.jpg" class="card-img-top" alt="..." style="width: 100px; height: 100px;"> - <div class="card-body"> - <h5 class="card-title">1 Month</h5> - <h6>10 000<img src="../../assets/items/pigcoin.png" style="width: 2rem"></h6> - <ShopButton - button-text="Buy item" - :disabled="10000 > points" - @click="buySomething()" - /> - </div> - </div> - <div class="card text-center d-flex justify-content-center align-items-center" style="width: 8rem; border: none"> - <img src="../../assets/items/pirbad.png" class="card-img-top" alt="..." style="width: 100px; height: 100px;"> - <div class="card-body"> - <h5 class="card-title">-10% rabatt</h5> - <h6>1000<img src="../../assets/items/pigcoin.png" style="width: 2rem"></h6> - <ShopButton - button-text="Buy item" - :disabled="1000 > points" - @click="buySomething()" - /> - </div> - </div> - </div> + </div> + <div class="col-md-12"> + <h1>Cool items</h1> + <div class="category row mb-2 m-2"> + <div class="card text-center d-flex justify-content-center align-items-center" + style="width: 8rem; border: none"> + <img src="../../assets/items/coffee.jpg" class="card-img-top" alt="..." + style="width: 100px; height: 100px;"> + <div class="card-body"> + <h5 class="card-title">Free Coffee</h5> + <h6>500<img src="../../assets/items/pigcoin.png" style="width: 2rem"></h6> + <ShopButton button-text="Buy item" :disabled="500 > points" @click="buySomething()" /> + </div> + </div> + <div class="card text-center d-flex justify-content-center align-items-center" + style="width: 8rem; border: none"> + <img src="../../assets/items/viaplay.jpg" class="card-img-top" alt="..." + style="width: 100px; height: 100px;"> + <div class="card-body"> + <h5 class="card-title">1 Month</h5> + <h6>10 000<img src="../../assets/items/pigcoin.png" style="width: 2rem"></h6> + <ShopButton button-text="Buy item" :disabled="10000 > points" @click="buySomething()" /> + </div> </div> + <div class="card text-center d-flex justify-content-center align-items-center" + style="width: 8rem; border: none"> + <img src="../../assets/items/pirbad.png" class="card-img-top" alt="..." + style="width: 100px; height: 100px;"> + <div class="card-body"> + <h5 class="card-title">-10% rabatt</h5> + <h6>1000<img src="../../assets/items/pigcoin.png" style="width: 2rem"></h6> + <ShopButton button-text="Buy item" :disabled="1000 > points" @click="buySomething()" /> + </div> + </div> + </div> + </div> + </div> + </div> + + <div class="modal fade" id="premiumModal" tabindex="-1" role="dialog" aria-labelledby="premiumModalLabel" + aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="premiumModalLabel">Premium Package</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <!-- Add premium package information here --> + <p>Unlock exclusive features with our Premium Package!</p> + <ul> + <li>Feature 1</li> + <li>Feature 2</li> + <li>Feature 3</li> + </ul> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + </div> + </div> + </div> + </div> + + <!-- Modal for Ad-free Package --> + <div class="modal fade" id="adfreeModal" tabindex="-1" role="dialog" aria-labelledby="adfreeModalLabel" + aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="adfreeModalLabel">Ad-free Package</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <!-- Add ad-free package information here --> + <p>Enjoy uninterrupted browsing with our Ad-free Package!</p> + <ul> + <li>No more annoying ads</li> + <li>Fast loading times</li> + <li>Exclusive content</li> + </ul> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + </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'; - import handleUnknownError from '@/components/Exceptions/unkownErrorHandler'; + </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'; +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler'; - let apiUrl = import.meta.env.VITE_APP_API_URL; - const products = ref([] as any); - const points = ref(); +let apiUrl = import.meta.env.VITE_APP_API_URL; +const products = ref([] as any); +const points = ref(); - /** - * Retrieves the store's products and updates the products list. - */ - const getStore = async () => { - try { - const response = await ItemService.getStore(); - products.value = response; - } catch (error) { - handleUnknownError(error); - console.log(error); - } +/** + * Retrieves the store's products and updates the products list. + */ +const getStore = async () => { + try { + const response = await ItemService.getStore(); + products.value = response; + } catch (error) { + handleUnknownError(error); + console.log(error); } +} - /** - * Retrieves the user's current points and updates the points reference. - */ - const getPoints = async () => { - try { - const response = await UserService.getUser(); - points.value = response.point?.currentPoints; - } catch (error) { - handleUnknownError(error); - console.log(error); - } +/** + * Retrieves the user's current points and updates the points reference. + */ +const getPoints = async () => { + try { + const response = await UserService.getUser(); + points.value = response.point?.currentPoints; + } catch (error) { + handleUnknownError(error); + console.log(error); } +} - /** - * Buys an item with the specified item ID. - * Sends a request to buy the item, then refreshes the store and points information. - * - * @param {number} itemId - The ID of the item to buy. - */ - const buyItem = async (itemId: number) => { - try { - await ItemService.buyItem({ itemId: itemId }); - await getStore(); - await getPoints(); - } catch (error) { - handleUnknownError(error); - console.log(error); - } +/** + * Buys an item with the specified item ID. + * Sends a request to buy the item, then refreshes the store and points information. + * + * @param {number} itemId - The ID of the item to buy. + */ +const buyItem = async (itemId: number) => { + try { + await ItemService.buyItem({ itemId: itemId }); + await getStore(); + await getPoints(); + } catch (error) { + handleUnknownError(error); + console.log(error); } +} - /** - * Buys a premium subscription for the user. - * Sends a request to update the user's subscription level to 'PREMIUM'. - * Updates the user's subscription level in the store. - */ - const buyPremium = async () => { - try { - await UserService.updateSubscriptionLevel({ subscriptionLevel: 'PREMIUM' }); - useUserInfoStore().setUserInfo({ - subscriptionLevel: 'PREMIUM', - }) - } catch (error) { - handleUnknownError(error); - console.log(error); - } +/** + * Buys a premium subscription for the user. + * Sends a request to update the user's subscription level to 'PREMIUM'. + * Updates the user's subscription level in the store. + */ +const buyPremium = async () => { + try { + await UserService.updateSubscriptionLevel({ subscriptionLevel: 'PREMIUM' }); + useUserInfoStore().setUserInfo({ + subscriptionLevel: 'PREMIUM', + }) + } catch (error) { + handleUnknownError(error); + console.log(error); } +} - /** - * Buys a subscription to remove ads for the user. - * Sends a request to update the user's subscription level to 'NO_ADS'. - * Updates the user's subscription level in the store. - */ - const buyNoAds = async () => { - try { - await UserService.updateSubscriptionLevel({ subscriptionLevel: 'NO_ADS' }); - useUserInfoStore().setUserInfo({ - subscriptionLevel: 'NO_ADS', - }) - } catch (error) { - handleUnknownError(error); - console.log(error); - } +/** + * Buys a subscription to remove ads for the user. + * Sends a request to update the user's subscription level to 'NO_ADS'. + * Updates the user's subscription level in the store. + */ +const buyNoAds = async () => { + try { + await UserService.updateSubscriptionLevel({ subscriptionLevel: 'NO_ADS' }); + useUserInfoStore().setUserInfo({ + subscriptionLevel: 'NO_ADS', + }) + } catch (error) { + handleUnknownError(error); + console.log(error); } +} /** * Generates a random code of the specified length. @@ -224,43 +273,42 @@ onMounted(() => { <style scoped> .card { - box-shadow: none; - margin: 10px; - border-radius: 8px; - padding-left: 5px; - padding-right: 5px; - height: 225px; + box-shadow: none; + margin: 10px; + border-radius: 8px; + padding-left: 5px; + padding-right: 5px; + height: 225px; } .box { - width: 90%; - justify-content: center; - text-align: center; - font-size: 5rem; - font-weight: 700; + 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); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card-body { - height: 100px; - padding: 5px; + height: 100px; + padding: 5px; } .col-md-12 { - border-bottom: 2px solid #000000; + border-bottom: 2px solid #000000; } #dropdownContainer { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 2rem; - flex-direction: column; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 2rem; + flex-direction: column; } -#background { -} +#background {} </style> \ No newline at end of file diff --git a/src/components/UserProfile/ExternalProfile.vue b/src/components/UserProfile/ExternalProfile.vue index 0baf368becd7ce4f3bd4129afd498bccabfa6d21..f95f18854632bb6294f5a90dde7e8bee5ee47eb1 100644 --- a/src/components/UserProfile/ExternalProfile.vue +++ b/src/components/UserProfile/ExternalProfile.vue @@ -5,6 +5,7 @@ import { useUserInfoStore } from "@/stores/UserStore"; import {UserService, BadgeService, GoalService, type GoalDTO, type BadgeDTO, FriendService} from "@/api"; import { ItemService } from "@/api"; import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' +import bannerImage from '@/assets/banners/stacked.svg' let apiUrl = import.meta.env.VITE_APP_API_URL; @@ -12,6 +13,8 @@ let firstname = ref(); let lastname = ref(); const imageUrl = ref(`../src/assets/userprofile.png`); +const bannerImageUrl = ref(bannerImage); + let hasBadges = ref(false) let hasInventory = ref(false) @@ -26,6 +29,8 @@ const streak = ref(0 as any); const isFriend = ref(false); const isRequestSent = ref(false); +const isMe = ref(false); + /** * Sets up the form for displaying user profile information. * Retrieves user profile data including first name, last name, points, streak, profile image, inventory, and badges. @@ -50,7 +55,12 @@ async function setupForm() { if (response.profileImage) { imageUrl.value = apiUrl + "/api/images/" + response.profileImage; } - getInventory(); + if (response.bannerImage != 0 && response.bannerImage !== null) { + console.log(response.bannerImage) + bannerImageUrl.value = apiUrl + "/api/images/" + response.bannerImage; + } + let userId = route.params.id; + isMe.value = String(userId) !== String(useUserInfoStore().id); getBadges(); } catch (err) { handleUnknownError(err) @@ -143,14 +153,34 @@ const removeFriend = () => { <div class="row d-flex justify-content-center align-items-center h-100"> <div class="col 12"> <div class="card"> - <div class="rounded-top text-white d-flex flex-row bg-primary" style="height:200px;" id="banner"> - <div class=" d-flex flex-column align-items-center justify-content-center"> - <img :src="imageUrl" alt="Generisk plassholderbilde" class="img-fluid img-thumbnail" - style="width: 150px; height:150px; margin-left: 25px; margin-right: 15px;"> + <div class="rounded-top text-white d-flex flex-row bg-primary justify-content-between" :style="{ + height: '200px', + backgroundImage: `url(${bannerImageUrl})`, + backgroundSize: 'cover', + backgroundRepeat: 'no-repeat' + }"> + <div class=" text-white d-flex flex-row"> + <div class=" d-flex flex-column align-items-center justify-content-center"> + <img :src="imageUrl" alt="Generisk plassholderbilde" class="img-fluid img-thumbnail" + style="width: 150px; height:150px; margin-left: 25px; margin-right: 15px;"> + </div> + <h1 data-cy="firstname" style="display: flex; align-items: end; margin-bottom: 20px;">{{ firstname }} {{ + lastname }}</h1> + </div> + <div class="d-flex align-items-end text-white my-3 mx-5"> + <div class="d-flex align-items-center flex-column"> + <p class="mb-1 h2 d-flex flex-column align-items-center" data-cy="points"><img + src="@/assets/items/pigcoin.png" style="width: 80px; height: 80px" data-toggle="tooltip" + title="Points"> {{ points }}</p> + </div> + <div class="d-flex align-items-center flex-column px-3"> + <p class="mb-1 h2 d-flex flex-column align-items-center" data-cy="streak"><img + src="@/assets/icons/fire.png" style="width: 80px; height: 80px" data-toggle="tooltip" + title="Points"> {{ streak }}</p> + </div> </div> - <h1 data-cy="firstname" style="display: flex; align-items: end; margin-bottom: 20px;">{{ firstname }} {{ lastname }}</h1> </div> - <div class="p-3 text-black" style="background-color: #f8f9fa;"> + <div v-if="isMe" class="p-3 text-black" style="background-color: #f8f9fa;"> <div class="d-flex justify-content-end text-center py-1"> <div style="width: 100%; display: flex; justify-content: start"> <button @@ -178,35 +208,6 @@ const removeFriend = () => { Fjern venn </button> </div> - <div> - <p class="mb-1 h2" data-cy="points">{{ points }} <img src="@/assets/items/pigcoin.png" style="width: 4rem"></p> - <p class="small text-muted mb-0">Poeng</p> - </div> - <div class="px-3"> - <p class="mb-1 h2" data-cy="streak">{{ streak }} <img src="@/assets/icons/fire.png" style="width: 4rem"></p> - <p class="small text-muted mb-0">Streak</p> - </div> - </div> - </div> - <hr> - <div class="card-body p-1 text-black"> - <div class="row"> - <div class="col"> - <div class="container-fluid"> - <h1 class="mt-1 text-start badges-text">Lageret ditt</h1> - <div v-if="hasInventory" class="scrolling-wrapper-badges row flex-row flex-nowrap mt-2 pb-2 pt-2"> - <div v-for="product in inventory" :key="product.id" class="card text-center" - style="width: 12rem; border: none; cursor: pointer; margin: 1rem; border: 2px solid black"> - <img :src="apiUrl + `/api/images/${product.imageId}`" class="card-img-top" - alt="..." /> - <div class="card-body"> - <h5 class="card-title">{{ product.itemName }}</h5> - </div> - </div> - </div> - <div v-else>Ingen gjenstander</div> - </div> - </div> </div> </div> <hr> diff --git a/src/components/UserProfile/MyProfile.vue b/src/components/UserProfile/MyProfile.vue index 6e20e5dbe909bd1fe270c5abd29c34a86c37fe5c..e6a4871922b77d5dd24d5903c11ce18e9645f492 100644 --- a/src/components/UserProfile/MyProfile.vue +++ b/src/components/UserProfile/MyProfile.vue @@ -168,8 +168,8 @@ const toUpdateUserSettings = () => { <div class="row d-flex justify-content-center align-items-center h-100"> <div class="col 12"> <div class="card"> - <div class="rounded-top text-white d-flex flex-row bg-primary justify-content-between" :style="{ - height: '200px', + <div class="rounded-top text-white d-flex flex-row bg-primary justify-content-between flex-wrap" id="banner" :style="{ + backgroundImage: `url(${bannerImageUrl})`, backgroundSize: 'cover', backgroundRepeat: 'no-repeat' @@ -180,7 +180,7 @@ const toUpdateUserSettings = () => { style="width: 150px; height:150px; margin-left: 25px; margin-right: 15px;"> </div> <h1 data-cy="firstname" style="display: flex; align-items: end; margin-bottom: 20px;">{{ firstname }} {{ - lastname }}</h1> + lastname }}</h1> </div> <div class="d-flex align-items-end text-white my-3 mx-5"> <div class="d-flex align-items-center flex-column"> @@ -250,7 +250,8 @@ const toUpdateUserSettings = () => { <div class="card-body"> <h5 class="card-title">{{ goals[index]['name'] }}</h5> <p class="card-text">{{ goals[index]['description'] }}</p> - <p class="card-text"><small class="text-muted">{{ goals[index]['targetAmount'] }}</small> + <p class="card-text"><small class="text-muted">{{ goals[index]['targetAmount'] + }}</small> </p> <a href="#" class="btn stretched-link" @click="toRoadmap"></a> </div> @@ -330,7 +331,13 @@ const toUpdateUserSettings = () => { } #banner { - background-image: url('/src/assets/banners/stacked.svg'); + height: 200px; +} + +@media (max-width: 940px) { + #banner { + height: 320px; +} } /*-------*/ @@ -338,6 +345,12 @@ const toUpdateUserSettings = () => { background-color: #00DBDE; } +.classyButton { + background-color: #003A58; + border: #003A58; + color: white; +} + .classyButton:hover { background-color: #003b58ec; border: #003A58; diff --git a/src/router/index.ts b/src/router/index.ts index 2d1284aa1da81f104b6d9a22c238228ccdf5d568..5d22daa14e0a814cca8b96f4ab79038b31bb128c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -28,6 +28,12 @@ const routes = [ name: 'profile', component: () => import('@/views/User/MyProfileView.vue'), }, + { + path: 'admin', + name: 'admin', + component: () => import('@/views/Admin/AdminDashboardView.vue'), + meta: { requiresAdmin: true } + }, { path: '/settings', name: 'settings', @@ -186,7 +192,7 @@ router.beforeEach((to, from, next) => { if (requiresAuth && !isAuthenticated) { next({ name: 'login', query: { redirect: to.fullPath } }); - } else if (requiresAdmin && userRole !== 'admin') { + } else if (requiresAdmin && userRole !== 'ADMIN') { next({ name: 'unauthorized' }); } else if (requiresPremium && userSubscription !== 'PREMIUM') { next({ name: 'home' }); diff --git a/src/views/Admin/AdminDashboardView.vue b/src/views/Admin/AdminDashboardView.vue new file mode 100644 index 0000000000000000000000000000000000000000..32398130be99dcff38e8c13d264b1cec6aa8c8e1 --- /dev/null +++ b/src/views/Admin/AdminDashboardView.vue @@ -0,0 +1,11 @@ +<script setup lang="ts"> +import AddminFeedback from "@/components/Admin/AdminFeedback.vue"; +</script> + +<template> + <AddminFeedback></AddminFeedback> +</template> + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/views/BasePageView.vue b/src/views/BasePageView.vue index 78e96a6d1707e650b91b86c2f0db53eef83fd8ce..09d24858a937e2d469bc8c6204bb4a5973bb1d5e 100644 --- a/src/views/BasePageView.vue +++ b/src/views/BasePageView.vue @@ -18,4 +18,10 @@ import { useUserInfoStore } from '@/stores/UserStore'; min-height: 700px; margin: 0 140px; } + +@media (max-width: 768px) { + #minHeight { + margin: 0 20px; + } +} </style> \ No newline at end of file diff --git a/src/views/User/UserFeedbackView.vue b/src/views/User/UserFeedbackView.vue index 3c28adbed9221b1b24edd8a33a6638531c293ffd..28a19b184a94fdede4f48198142ccf53da06e92d 100644 --- a/src/views/User/UserFeedbackView.vue +++ b/src/views/User/UserFeedbackView.vue @@ -2,15 +2,25 @@ <main> <div class="wrapper"> <div id="formFrame"> - <h1>TIlbakemelding</h1> - <form @submit.prevent="submitForm"> - <BaseInput v-model="email" label="Email" type="email" placeholder="Enter your email" inputId="email" required /> + <h1>Tilbakemelding</h1> + <form ref="formRef" id="loginForm" @submit.prevent="submitForm" novalidate> + <BaseInput :model-value="emailRef" + @input-change-event="handleEmailInputEvent" + id="emailInput" + input-id="email" + type="email" + label="E-post" + placeholder="Skriv inn din e-post" + invalid-message="Ugyldig e-post" + /> + <br> <label for="feedback">Din tilbakemelding:</label> - <textarea v-model="message" placeholder="Write here" rows="5" name="comment[text]" id="comment_text" cols="33" + <textarea v-model="messageRef" placeholder="Skriv meldingen din her" rows="5" name="comment[text]" id="comment_text" cols="33" required></textarea> - <BaseButton button-text="Send" @click="submitForm">Send inn</BaseButton> - <p v-if="submissionStatus">{{ submissionStatus }}</p> + <p data-cy="change-email-msg-error" class="text-danger">{{ errorMsg }}</p> + <BaseButton button-text="Send" @click="submitForm" style="padding: 10px 30px; font-size: 18px; font-weight: normal;">Send inn</BaseButton> + <p data-cy="change-email-msg-confirm" class="text-success">{{ confirmationMsg }}</p> </form> </div> </div> @@ -21,13 +31,33 @@ import { ref } from 'vue'; import BaseInput from '@/components/BaseComponents/Input/BaseInput.vue'; import BaseButton from '@/components/BaseComponents/Buttons/BaseButton.vue'; +import { type FeedbackRequestDTO, UserService } from '@/api' +import handleUnknownError from '@/components/Exceptions/unkownErrorHandler' + +const emailRef = ref(""); +const messageRef = ref(""); +const errorMsg = ref('') +const confirmationMsg = ref('') -const email = ref(""); -const message = ref(""); -const submissionStatus = ref(""); +const handleEmailInputEvent = (newValue: any) => { + emailRef.value = newValue +} const submitForm = async () => { - + try { + const feedbackRequest: FeedbackRequestDTO = { + email: emailRef.value, + message: messageRef.value + }; + console.log("feedbackRequest", feedbackRequest); + UserService.sendFeedback({ requestBody: feedbackRequest }); + messageRef.value = '' + errorMsg.value = '' + confirmationMsg.value = 'Tilbakemeldingen ble sendt!' + } catch (err) { + errorMsg.value = handleUnknownError(err); + confirmationMsg.value = '' + } }; </script> diff --git a/src/views/User/UserSettingsView.vue b/src/views/User/UserSettingsView.vue index 5f2dc15fbeb8c0e7e8f35d32c4d6455b2a4ff933..431cc2148d255946ef13f062283376d825f1f0ba 100644 --- a/src/views/User/UserSettingsView.vue +++ b/src/views/User/UserSettingsView.vue @@ -107,7 +107,8 @@ function toBilling() { <div class="card-header border-bottom mb-3 d-flex d-md-none"> <ul class="nav nav-tabs card-header-tabs nav-gap-x-1" role="tablist"> <li class="nav-item"> - <a href="#" data-toggle="tab" class="nav-link has-icon active"><svg + <a @click.prevent="setActive('/settings/profile')" @click="toProfile" + :class="['nav-item nav-link has-icon', { 'nav-link-faded': useRoute().path !== '/settings/profile', 'active': useRoute().path === '/settings/profile' }]"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"> @@ -116,7 +117,8 @@ function toBilling() { </svg></a> </li> <li class="nav-item"> - <a href="#" data-toggle="tab" class="nav-link has-icon"><svg + <a @click.prevent="setActive('/settings/account')" @click="toAccount" + :class="['nav-item nav-link has-icon', { 'nav-link-faded': useRoute().path !== '/settings/account', 'active': useRoute().path === '/settings/account' }]"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"> @@ -127,7 +129,8 @@ function toBilling() { </svg></a> </li> <li class="nav-item"> - <a href="#" data-toggle="tab" class="nav-link has-icon"><svg + <a @click.prevent="setActive('/settings/security')" @click="toSecurity" + :class="['nav-item nav-link has-icon', { 'nav-link-faded': useRoute().path !== '/settings/security', 'active': useRoute().path === '/settings/security' }]"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"> @@ -135,16 +138,8 @@ function toBilling() { </svg></a> </li> <li class="nav-item"> - <a href="#" data-toggle="tab" class="nav-link has-icon"><svg - xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" - fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" - stroke-linejoin="round" class="feather feather-bell"> - <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path> - <path d="M13.73 21a2 2 0 0 1-3.46 0"></path> - </svg></a> - </li> - <li class="nav-item"> - <a href="#" data-toggle="tab" class="nav-link has-icon"><svg + <a @click.prevent="setActive('/settings/bank')" @click="toBilling" + :class="['nav-item nav-link has-icon', { 'nav-link-faded': useRoute().path !== '/settings/bank', 'active': useRoute().path === '/settings/bank' }]"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-credit-card">