diff --git a/cypress/e2e/navbar.cy.js b/cypress/e2e/navbar.cy.js index 2cbd9b1ad9230819d7890632705bb94da06736a4..9e227781986c499d749fd2f66f66d95f56a4ee32 100644 --- a/cypress/e2e/navbar.cy.js +++ b/cypress/e2e/navbar.cy.js @@ -1,6 +1,5 @@ describe('Correct navigation links', () => { - /*TODO*/ }) describe('Navbar on all pages', () => { - /*TODO*/ -}) \ No newline at end of file + +}) diff --git a/src/components/EditAccount.vue b/src/components/EditAccount.vue new file mode 100644 index 0000000000000000000000000000000000000000..564f678621ce2e4e4873b4ea42d473d54184630d --- /dev/null +++ b/src/components/EditAccount.vue @@ -0,0 +1,198 @@ +<template> + <h2>Konto-innstillinger</h2> + <form @submit.prevent="submit"> + <p class="infoText">OBS: Kontakt admin dersom du ønsker Ã¥ oppdatere epost</p><br> + + <p id="emailField">Epost: {{this.account.email}}</p><br> + + <label for="fname">Endre fornavn</label><br> + <input type="text" id="fname" v-model="updatedAccount.upFirstname"><br> + + <label for="password">Endre passord</label><br> + <input type="password" id="password" v-model="updatedAccount.upPassword"> + + <button class="greenBtn" @click="saveAccountSettings">Lagre profilendringer</button> + <p id="alert">{{alertMsg}}</p> + </form> + + <div id="logout"> + <p class="infoText">Logger deg ut fra din SmartMat-konto</p> + <button class ="redBtn" @click="logOut">Logg ut</button> + </div> + + + <br> + <br> + <hr> + + <form @submit.prevent="submit" id = "dangerZone"> + <h1>🔺FARESONE🔺</h1> + <p class="infoText">Ved Ã¥ trykke pÃ¥ knappen nedenfor, vil du slette din SmartMat-konto</p> + <input type="checkbox" id="deletionCheckbox" v-model="deletionConfirmation"> + <label for="deletionCheckbox"> Jeg bekrefter jeg skjønner dette, og ønsker Ã¥ slette kontoen min SmartMat-konto for alltid.</label><br> + <button class="darkRedBtn" id ="delAccount" @click="deleteAccount">SLETT KONTO</button> + <p id="alert">{{delAlertMsg}}</p> + </form> +</template> + +<script> +import {mapState, mapStores} from "pinia"; +import {API} from "@/util/API"; +import { useAuthStore } from "@/stores/authStore"; +import router from "../router"; +export default { + name: "EditAccount", + computed: { + ...mapState(useAuthStore, ['account']), + ...mapStores(useAuthStore), + updatedAccount() { + return { + upFirstname: this.account.firstname, + upPassword:'', + } + }, + }, + data() { + return { + alertMsg:'', //message at the bottom of the form where you change account firstname and password + deletionConfirmation: false, + delAlertMsg:'', //message in the 'dager zone' + } + }, + methods: { + saveAccountSettings(){ //passord + const id = this.account.id; + + let newPassword = null + let newFirstName = null; + + //checks if username and password have been changed + if(this.updatedAccount.upPassword.length!==0) { + newPassword = this.updatedAccount.upPassword; + } + //firstName won't be changed if empty + if(this.updatedAccount.upFirstname!==''){ + newFirstName = this.updatedAccount.upFirstname; + } + + API.updateAccount( + id,{ + firstname:newFirstName, + password:newPassword, + } + ).then((savedAccount)=>{ + useAuthStore().setAccount(savedAccount); + this.alertMsg = "Konto oppdatert." + }).catch(()=> { + this.alertMsg = "‼ï¸Det oppsto en feil.â€¼ï¸ " + }) + }, + deleteAccount(){ + if(this.deletionConfirmation===false){ + this.delAlertMsg = "‼ï¸Du mÃ¥ bekrefte at du vil slette konto ved Ã¥ huke av boksen‼ï¸" + } + else { + const id = this.account.id; + API.deleteAccount( + id + ).then(()=>{ + router.push('/login') + }).catch(()=> { + this.delAlertMsg = "‼ï¸Det oppsto en feil ved sletting av bruker‼ï¸" + }) + } + }, + logOut(){ + useAuthStore().logout(); + router.push('/login') + }, + } + } +</script> + +<style scoped lang="scss"> +#dangerZone { + color: darkred; +} + +#logout{ + background-color: base.$grey; + color: black; + padding: 2em; + margin-top: 2em; + margin-bottom: 2em; + display: flex; + flex-direction: column; +} + +input[type="checkbox"] { + width: 2.5em; + height: 2.5em; +} + + + +/*--General--*/ +form { + background-color: base.$grey; + color: black; + align-content: end; + padding: 2em; + margin-top: 2em; + margin-bottom: 2em; +} + +input[type="text"], +input[type="password"]{ + width: 100%; + padding: .5em; +} + +button { + color: black; + border: 1px solid black; + margin: 1em; + padding:.9em; + font-weight: bold; +} +.redBtn { + background-color: base.$red; + border:none; + color:white; +} + +.redBtn:hover { + background-color: base.$red-hover; +} + +.darkRedBtn { + background-color: darkred; + border:none; + color:white; +} +.darkRedBtn:hover { + background-color: base.$darkred-hover; +} + +.greenBtn{ + background-color: base.$green; + border:none; + color:white; +} +.greenBtn:hover{ + background-color: base.$green-hover; +} + +.infoText { + background-color: white; + padding: .5em; + margin: .4em; +} + +#alert { + display: flex; + width:100%; + justify-content: center; + font-weight: bold; + } +</style> \ No newline at end of file diff --git a/src/components/EditProfile.vue b/src/components/EditProfile.vue new file mode 100644 index 0000000000000000000000000000000000000000..8725cfe22726c81985687fb8185ed436f4f12b77 --- /dev/null +++ b/src/components/EditProfile.vue @@ -0,0 +1,262 @@ +<template> + <h2>Profilinnstillinger</h2> + + + <div v-if="hasProfileImage" id = "profilepicture-container"> + <img width="100" :src="this.updatedProfile.upImage" alt="profile picture"> + </div> + <div v-else id="#placeholder"> + <Icon icon="material-symbols:person" :color=iconColor :style="{ fontSize: '500px'}" /> + </div> + + <h3>{{this.profile.name}}</h3> + <button @click="changeProfile" id="changeUserBtn" class="redBtn">Bytt bruker</button> + + <form @submit.prevent="submit"> + <label for="brukernavn">Profilnavn</label><br> + <input type="text" required id="brukernavn" v-model="this.updatedProfile.upName"><br> + <br> + <h3>Brukertype</h3> + <input type="radio" id="normal" value="false" name="restrict" v-model="this.updatedProfile.upRestricted"> + <label for="normal"> Standard</label><br> + + <input type="radio" id="restricted" value="true" name="restrict" v-model="this.updatedProfile.upRestricted"> + <label for="restricted"> Begrenset - Kan ikke redigere ukeplan eller handleliste</label><br><br> + + + <h3>Profilbilde</h3><br> + <div id="changeUserImage"> + <div v-if="hasProfileImage" id = "profilepicture-container"> + <img width="50" :src="this.updatedProfile.upImage" alt="profile picture"> + </div> + <div v-else id = "profilepicture-container"> + <Icon icon="material-symbols:person" :color=iconColor :style="{ fontSize: '30px'}" /> + </div> + + <label for="chooseImageUrl">Bilde-URL:</label><br> + <!--<input type="file" id="chooseImage" v-on:change="updateImage">--> + <input type="text" id="chooseImageUrl" v-model="this.updatedProfile.upImage"> + + </div> + <br><br> + <div id = "submitbuttonBox"> + <button class="greenBtn" @click=" saveUserSettings">Lagre profilendringer</button> + <button class="darkRedBtn" @click="deleteProfile">Slett brukerprofil</button> + </div> + + <p id="alert">{{alertMsg}}</p> + + </form> + +</template> + +<script> +import {mapState, mapStores} from "pinia"; +import {Icon} from "@iconify/vue"; +import {API} from "@/util/API"; +import { useAuthStore } from "@/stores/authStore"; +import router from "../router"; +export default { + name: "EditProfile", + components: {Icon}, + data() { + return { + alertMsg:'', + initialName: '', //used to compare with updated values + initialRestriction: '', + } + }, + computed: { + ...mapState(useAuthStore, ['profile']), + ...mapStores(useAuthStore), + updatedProfile() { + return { + upName: this.profile.name, + upRestricted: this.profile.restricted, + upImage: this.profile.profileImageUrl, + } + }, + iconColor() { + return "#D9D9D9" + }, + hasProfileImage() { + return false; + } + }, + beforeMount() {//used to compare with changed values + this.initialName = this.profile.name; + this.initialRestriction = this.profile.restricted; + }, + methods: { + changeProfile(){ + router.push("/selectProfile"); + }, + async deleteProfile() { + const id = this.profile.id; + API.deleteProfile(id).then(() => { + router.push('/selectProfile') + }).catch((_) => { + this.alertMsg = "‼ï¸Alle kontoer mÃ¥ ha minst en profil (profil ble ikke slettet)‼ï¸" + }) + }, + saveUserSettings(){ + const id = this.profile.id; + + let newName = null; + let newRestricted = null; + + if(this.updatedProfile.upName !== this.initialName){ + newName = this.updatedProfile.upName + } + if(this.updatedProfile.upRestricted !== this.initialRestriction){ + newRestricted = this.updatedProfile.upRestricted + } + + API.updateProfile( + id,{ + name:newName, + profileImageUrl:this.updatedProfile.upImage, + isRestricted: newRestricted, + } + ).then((savedProfile)=>{ + useAuthStore().setProfile(savedProfile); + this.alertMsg = "Profil oppdatert." + }).catch(error=> { + console.log(error) + if (error.message === '400') { + if(newRestricted){ + this.alertMsg = '‼ï¸Det oppsto en feil: Sørg for at det finnes mist en standard profil pÃ¥ kontoenâ€¼ï¸ ' + } else if(this.updatedProfile.name !== this.initialName || this.updatedProfile.name) { + this.alertMsg = '‼ï¸Det oppsto en feil: Det finnes allerede en bruker med samme navn‼ï¸' + } + }else{ + this.alertMsg = "‼ï¸Det oppsto en feil.‼ï¸" + } + }) + }, + + + + + + updateImage(){ + //todo update image preview + }, + chooseProfilePicture(){ + this.alertMsg = "skriv inn bildelenke i feltet, og oppdater innstillinger" + }, + } +} + +</script> + +<style scoped lang="scss"> + +input[type="radio"] { + width: 2em; + height: 2em; + +} + +#changeUserBtn { + border: 1px solid black; +} + +#profilepicture-container { + display:flex; + border-radius:50%; + width:100px; + height: 100px; + background-color: white; + justify-content: center; + align-items: center; + border: 3px solid base.$grey; +} + +#changeUserImage { + display:flex; +} + +img { + border-radius: 50%; +} + +#changeUserImage #profilepicture-container { + width: 50px; + height: 50px; +} + +#submitbuttonBox { + display:flex; + justify-content: space-between; +} + + + + + +/*--General--*/ +form { + background-color: base.$grey; + color: black; + align-content: end; + padding: 2em; + margin-top: 2em; + margin-bottom: 2em; + } + +input[type="text"], +input[type="password"]{ + width: 100%; + padding: .5em; +} + +button { + color: black; + border: 1px solid black; + margin: 1em; + padding:.9em; + font-weight: bold; +} +.redBtn { + background-color: base.$red; + border:none; + color:white; +} + +.redBtn:hover { + background-color: base.$red-hover; +} + +.darkRedBtn { + background-color: darkred; + border:none; + color:white; +} +.darkRedBtn:hover { + background-color: base.$darkred-hover; +} + +.greenBtn{ + background-color: base.$green; + border:none; + color:white; +} +.greenBtn:hover{ + background-color: base.$green-hover; +} + +.infoText { + background-color: white; + padding: .5em; + margin: .4em; +} + +#alert { + display: flex; + width:100%; + justify-content: center; + color: base.$light-green; + font-weight: bold; +} +</style> \ No newline at end of file diff --git a/src/components/FridgeItem.vue b/src/components/FridgeItem.vue index 1d58f426274b49c89a218ce4b94c9431aab99ba4..a23603f03c0e965368cda1ebea78fdf8865bb36f 100644 --- a/src/components/FridgeItem.vue +++ b/src/components/FridgeItem.vue @@ -2,7 +2,9 @@ <div id ="item"> <img :src="getImage" alt=""> <div id="itemInfo"> - <p id="fridgeItemName">{{this.actualItem.item.name }} {{ this.actualItem.amount.quantity }}{{this.actualItem.item.amount.unit}}</p> + <p id="fridgeItemName">{{ this.fridgeItem.item.name }} {{ + this.fridgeItem.amount.quantity + }}{{ this.fridgeItem.item.amount.unit }}</p> <p class="expText" :style="{color:expirationTextColor}">{{expirationText}}</p> </div> <div id = "appleBtn" @click="appleBtnPressed"> @@ -35,7 +37,7 @@ export default { } }, getImage(){ - return this.actualItem.item.image_url; + return this.fridgeItem.item.image_url; }, expirationText() { @@ -65,11 +67,7 @@ export default { } }, props: { - item: { - type:Object, - required: false, - }, - actualItem: { + fridgeItem: { type: Object, required:false, }, @@ -81,7 +79,7 @@ export default { }, methods: { getDateDifference(){ //returns the difference in days between the expiration date and today - let date = this.actualItem.exp_date; + let date = this.fridgeItem.exp_date; const epDate = new Date(date); const parsedDate = Date.parse(epDate) @@ -93,10 +91,10 @@ export default { return numOfDays; }, appleBtnPressed(){ - this.$emit('appleBtnPressed', this.actualItem); + this.$emit('appleBtnPressed', this.fridgeItem); }, formatDate(){ //formats expiration date as dd.mm.yyyy - let fullExpirationDate = new Date(this.actualItem.exp_date); + let fullExpirationDate = new Date(this.fridgeItem.exp_date); let day = fullExpirationDate.getDate(); let month= (fullExpirationDate.getMonth()+1).toString(); let year= fullExpirationDate.getFullYear().toString(); diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 6e2e12e21b564f1fa9ec4ec8c75ee96072e5b47c..10d7e10ab9c318905ccc11056c64963d6d86ca50 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -23,8 +23,8 @@ </RouterLink> </li> <li> - <RouterLink :to="'/'" :aria-label="'link to settings page'"> - <Icon icon="mdi:cog" :color="iconColor" :style="{ fontSize: iconSize }"/> + <RouterLink :to="'/profileSettings'" :aria-label="'link to settings page'"> + <Icon id="settingsIcon" icon="mdi:cog" :color="iconColor" :style="{ fontSize: iconSize }"/> </RouterLink> </li> </ul> @@ -34,7 +34,6 @@ <script> import { Icon } from '@iconify/vue'; import Logo from "@/components/Logo.vue"; -import { RouterLink } from 'vue-router' export default { name: "Navbar", @@ -46,6 +45,9 @@ export default { iconSize() { return `32px`; }, + logoSize() { + return '52px'; + } } } </script> diff --git a/src/components/__tests__/EditAccount.spec.js b/src/components/__tests__/EditAccount.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..994d431aa651c86b61e34d0a430cf959a6ea0350 --- /dev/null +++ b/src/components/__tests__/EditAccount.spec.js @@ -0,0 +1,47 @@ +import {describe, it, expect, vi} from 'vitest' +import { mount } from '@vue/test-utils' +import EditAccount from "@/components/EditAccount.vue"; +import {createTestingPinia} from "@pinia/testing"; +import {useAuthStore} from "@/stores/authStore"; +describe('Behaves as expected', () => { + const wrapper = mount(EditAccount, { + global: { + plugins: [createTestingPinia({ + createSpy: vi.fn, + })], + }, + }); + + const store = useAuthStore() + + store.account = { + id: "1", + email:"epost@epost.no", + firstname:"Ola", + password: "Ola123", + fridge: {}, + } + + it('Has email field that contains the account email', async () => { + expect(wrapper.find('#emailField').text()).toContain('epost@epost.no'); + }) + + it('Has firstname field with current firstname', async () => { + expect(wrapper.vm.updatedAccount.upFirstname).to.equal('Ola'); + const fnameInput = wrapper.find('#fname'); + expect(fnameInput.element.value).to.equal('Ola'); + }) + + it('Password field is empty', async () => { + const passwordInput = wrapper.find('#password'); + expect(passwordInput.element.value).to.equal(''); + }) + + it('attempting to delete account without checking box results in error message', async () => { + await wrapper.find('#delAccount').trigger('click'); + const alertMsg = wrapper.vm.delAlertMsg; + expect(alertMsg).to.contain('boks'); + + }) + +}) diff --git a/src/components/__tests__/EditProfile.spec.js b/src/components/__tests__/EditProfile.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f78db12586fa2a935f9067aba276d8dbfa1e8cf3 --- /dev/null +++ b/src/components/__tests__/EditProfile.spec.js @@ -0,0 +1,64 @@ +import { mount } from '@vue/test-utils' +import { describe, it, expect, vi } from 'vitest' +import { createTestingPinia } from '@pinia/testing' +import {useAuthStore} from "@/stores/authStore"; +import EditProfile from '@/components/EditProfile.vue' +describe('EditProfile', () => { + const pinia = createTestingPinia({ + initialState: { + profile: { name: 'Ola',restricted:false,profileImageUrl:"some/valid/image.png" }, + }, + createSpy: vi.fn(), + }) + + const wrapper = mount(EditProfile, { + global: { + plugins: [pinia], + }, + }) + + const store = useAuthStore(pinia) + store.profile = { + name:"Ola", + restricted:true, + profileImageUrl:"some/valid/image.png" + } + + it('Profile name is on profile page', () => { + expect(wrapper.vm.updatedProfile.upName).toContain('Ola') + const unameInput = wrapper.find('#brukernavn'); + expect(unameInput.element.value).to.contain('Ola'); + }) + + it('If profile.restricted is true, then radio input with value false is not selected', () => { + const radioInput = wrapper.find('input[type=radio][value="false"]') + expect(radioInput.element.checked).toBe(false) + }) + + it('If profile.restricted is true, then radio input with valuetrue *is* selected', () => { + const radioInput = wrapper.find('input[type=radio][value="true"]') + expect(radioInput.element.checked).toBe(true) + }) + + //update the value from restricted true -> false + it('After changing restricted radio, the values are updated too', async () => { + const notRestrictedRadioInput = wrapper.find('#normal') + const restrictedRadioInput = wrapper.find('#restricted') + + expect(notRestrictedRadioInput.element.checked).toBe(false) + expect(restrictedRadioInput.element.checked).toBe(true) + + await notRestrictedRadioInput.trigger('click') + + expect(notRestrictedRadioInput.element.checked).toBe(true) + expect(restrictedRadioInput.element.checked).toBe(false) + + await wrapper.vm.$nextTick() + + setTimeout(() => { + expect(wrapper.vm.updatedProfile.upRestricted).toBe(false) + }, 1000); + }) + + +}) \ No newline at end of file diff --git a/src/components/__tests__/FridgeItem.spec.js b/src/components/__tests__/FridgeItem.spec.js index 5a7b241b63107426dc1f3aad53e793c412764e74..2760bed18450440eafcbaac7a8dc29823a0235b3 100644 --- a/src/components/__tests__/FridgeItem.spec.js +++ b/src/components/__tests__/FridgeItem.spec.js @@ -28,7 +28,7 @@ describe('Fridge items render correctly', () => { it('displays the name of the item', () => { const wrapper = mount(FridgeItem, { props: { - actualItem: normalItem, + fridgeItem: normalItem, }, }); expect(wrapper.text()).toContain('eple'); @@ -37,7 +37,7 @@ describe('Fridge items render correctly', () => { it('displays the amount of the item in the fridge' , () => { const wrapper = mount(FridgeItem, { props: { - actualItem: normalItem, + fridgeItem: normalItem, }, }); expect(wrapper.text()).toContain('6'); @@ -46,7 +46,7 @@ describe('Fridge items render correctly', () => { it('displays item image', () => { const wrapper = mount(FridgeItem, {props: { - actualItem: normalItem, + fridgeItem: normalItem, }, }); const itemImage = wrapper.find('img'); @@ -58,7 +58,7 @@ describe('Fridge items render correctly', () => { it('displays text of different color when item has expired', () => { const wrapper = mount(FridgeItem, { props: { - actualItem: expiredItem, + fridgeItem: expiredItem, }, }); @@ -71,7 +71,7 @@ describe('Behaves as expected', () => { it('emits when the apple button is pressed', async () => { const wrapper = mount(FridgeItem, { props: { - actualItem: normalItem, + fridgeItem: normalItem, }, }); diff --git a/src/components/images/w66XcIlw.jpeg b/src/components/images/w66XcIlw.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f27be0d3372e38997dea9ea9913ad851e4f59048 Binary files /dev/null and b/src/components/images/w66XcIlw.jpeg differ diff --git a/src/router/index.js b/src/router/index.js index cf14f5330cb9f4cca178d0d7db316441fe80727f..aa8efdeb78b72d4e113c4613dbd546c9c8632f8a 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,4 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' +import ProfileSettings from "@/views/SettingsView.vue"; +import MissingPage from "@/views/MissingPage.vue"; import HomeView from '../views/HomeView.vue' import LoginView from '../views/LoginView.vue' import SelectProfileView from '../views/SelectProfileView.vue' @@ -58,6 +60,16 @@ const router = createRouter({ name: 'shoppingList', component: ShoppingListView }, + { + path: '/profileSettings', + name: 'profileSettings', + component: ProfileSettings + }, + { + path: '/:catchAll(.*)', + name: "404 Page not found" , + component: MissingPage } + ] }) diff --git a/src/stores/authStore.js b/src/stores/authStore.js index 0e906aa283730aea3cec044f69bcaf8d5827f0e1..db029beb454bad9b49dcfc0c1ac658915888f9fa 100644 --- a/src/stores/authStore.js +++ b/src/stores/authStore.js @@ -45,6 +45,11 @@ export const useAuthStore = defineStore("auth", { }, addItemToFridge(fridgeItem){ this.fridgeItems.push(fridgeItem); + }, + updateProfile(name, image, isRestricted){ + this.profile.name = name; + this.profile.profileImageUrl = image; + this.profile.restricted = isRestricted; } } }); diff --git a/src/stores/useFoodPreferenceStore.js b/src/stores/useFoodPreferenceStore.js new file mode 100644 index 0000000000000000000000000000000000000000..028f2cd981f181f9eeb5b4c6a22403e11e9130e4 --- /dev/null +++ b/src/stores/useFoodPreferenceStore.js @@ -0,0 +1,21 @@ +import {API} from "@/util/API" +import { defineStore} from "pinia"; + +export const useFoodPreferenceStore = defineStore("foodPreference", { + state: () => { + return { + foodPreferences: [], + }; + }, + persist: { + storage: localStorage, + }, + actions: { + fetchAllOptions() { + API.getFoodpreferences() + .then((foodPreferences) => { + this.foodPreferences = foodPreferences; + }) + } + } +}) \ No newline at end of file diff --git a/src/style.scss b/src/style.scss index 9a2b66fdb2767791e8529c854f69a6df196f92ea..9cc6bdcfdd7f3e718539c048fd98eaf01b6bffb0 100644 --- a/src/style.scss +++ b/src/style.scss @@ -5,10 +5,12 @@ $white:#FFFFFF; $grey:#D9D9D9; $light-grey: #F3F4F9; $red:#EE6D6D; +$darkred: darkred; $red-hover: darken( $red, 5% ); $green-hover: darken( $green, 8% ); $light-green-hover: darken( $light-green, 10% ); +$darkred-hover: darken(darkred, 10%); $indigo: #2c3e50; $desktop-min: 1024px; diff --git a/src/util/API.js b/src/util/API.js index ca7ce08d492a9d7d4bf925de49c83d63bee96796..48507b710f51989fa41da77523087c1abb8abe0f 100644 --- a/src/util/API.js +++ b/src/util/API.js @@ -67,7 +67,7 @@ export const API = { /** * Sends the user into the home page logged in as the profile specified - * @param id ID of the profile that the user will log in as + * @param id ID of the profile that the user will log in as */ selectProfile: async (id) => { const authStore = useAuthStore() @@ -234,7 +234,6 @@ export const API = { axios.put(`${import.meta.env.VITE_BACKEND_URL}/fridge/ingredientsAmount`, request,{ headers: { Authorization: `Bearer ${authStore.token}` }, }).then((response) => { - authStore.setFridge(response.data); return response.data; }).catch(()=> { throw new Error("Could modify ingredient. "); @@ -301,11 +300,11 @@ export const API = { */ deleteItemFromShoppingList: async (ingredientId) => { const authStore = useAuthStore(); - return axios.delete(import.meta.env.VITE_BACKEND_URL + '/shoppinglist/items/' + ingredientId, + return axios.delete(import.meta.env.VITE_BACKEND_URL + '/shoppinglist/items/' + ingredientId, { headers: {Authorization: "Bearer " + authStore.token } }) - .then(response => { return response.data; }) + .then(response => { return response.data; }) .catch(err => {console.log(err)}) }, @@ -320,7 +319,7 @@ export const API = { { headers: { Authorization: "Bearer " + authStore.token } }) - .then(response => { return response.data; }) + .then(response => { return response.data; }) .catch(err => {console.log(err)}) }, @@ -389,5 +388,112 @@ export const API = { .catch((error) => { throw new Error(error); }) - } + }, + + + /** + * Deletes account from the + * @param id + * @param token + * @returns {Promise<*>} + */ + deleteAccount: async (id) => { + const authStore = useAuthStore(); + if (!authStore.isLoggedIn) { + throw new Error(); + } + + return axios.delete(`${import.meta.env.VITE_BACKEND_URL}/account/${id}`, { + headers: { Authorization: `Bearer ${authStore.token}` }, + }) + .then(() => { + authStore.logout() + router.push('/login') + }) + .catch((error) => { + throw new Error(error); + }); + + }, + + /** + * + * @param id account id + * @param request password and firstname + * @returns {Promise<*>} + */ + updateAccount: async (id, request) => { + const authStore = useAuthStore(); + if (!authStore.isLoggedIn) { + throw new Error(); + } + + return axios.put(`${import.meta.env.VITE_BACKEND_URL}/account/${id}`,request, { + headers: { Authorization: `Bearer ${authStore.token}` }, + }) + .then((response) => { + authStore.setAccount(response.data) + return response.data; + }).catch(() => { + throw new Error("Error when updating account: "); + }); + }, + + /** + * Updates the profile name, restriction and profile image + * If error: "error.message" returns the status code + * @param id profile id + * @param request + * @returns {Promise<*>} + */ + updateProfile: async (id, request) => { + const authStore = useAuthStore(); + if (!authStore.isLoggedIn) { + throw new Error(); + } + + return axios.put(`${import.meta.env.VITE_BACKEND_URL}/profile/${id}`,request, { + headers: { Authorization: `Bearer ${authStore.token}` }, + + }) + .then((response) => { + authStore.setProfile(response.data) + return response.data; + }) + .catch((error) => { + throw new Error(error.response.status); + }); + + }, + + /** + * Deletes a profile from an account + * @param id + * @param request + * @returns {Promise<*>} + */ + deleteProfile: async (id) => { + const authStore = useAuthStore(); + if (!authStore.isLoggedIn) { + throw new Error(); + } + + return axios.delete(`${import.meta.env.VITE_BACKEND_URL}/profile/${id}`, { + headers: { Authorization: `Bearer ${authStore.token}` }, + }) + .then(() => { + router.push('/selectProfile') + }) + .catch(() => { + throw new Error("Kan ikke slette profil"); + }); + + }, + + + + + + + } \ No newline at end of file diff --git a/src/views/FridgeView.vue b/src/views/FridgeView.vue index 365092cf95cba4cb1f45bd5a066194ebf87bbe39..6ee345d55cce6d76d913784118d191047ede2ab7 100644 --- a/src/views/FridgeView.vue +++ b/src/views/FridgeView.vue @@ -6,7 +6,7 @@ <eat-fridge-item-modal @closeModal="hideModal" v-if="visible" :fridge-item="selectedItem"></eat-fridge-item-modal> <div id = "itemContainer" > - <FridgeItem v-for="item in fridgeItems" :actualItem = "item" @appleBtnPressed="showModal"></FridgeItem> + <FridgeItem v-for="item in fridgeItems" :fridgeItem = "item" @appleBtnPressed="showModal"></FridgeItem> </div> <div id="addItemBtn-container"> diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index af7052980cac33be13f5f373d144934daaa8f204..3cf09945743a39413906a51730ebe43d5644685b 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,6 +1,7 @@ <script> import { useAuthStore } from "@/stores/authStore.js"; import { mapState } from 'pinia' + import router from "@/router"; import GroceryOffers from "@/components/GroceryOffers.vue"; export default { @@ -13,8 +14,6 @@ computed: { ...mapState(useAuthStore, ['profile']) } - - } </script> diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 17694f15d73d1c73d27affc3f75fcc27bfa6a233..2a0b5b48cf99b00c92a1956c4965c477c7c1c63b 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -13,7 +13,6 @@ }, methods: { login() { - //todo: implement when API is up API.login({email: this.email, password: this.password}).then(() => { router.push("/selectProfile"); }) diff --git a/src/views/MissingPage.vue b/src/views/MissingPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..e110350375e236a41a035386fb3a907cdccc37e9 --- /dev/null +++ b/src/views/MissingPage.vue @@ -0,0 +1,15 @@ +<template> + <h1 id="msg">404</h1> + <h2>Oida, denne siden finnes ikke</h2> + <RouterLink to="/">GÃ¥ tilbake til forsiden</RouterLink> +</template> + +<script> +export default { + name: "MissingPage" +} +</script> + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/views/SelectProfileView.vue b/src/views/SelectProfileView.vue index d8bd68c631b574e531cf4a7e7381c9c254d2435b..8dd7991f2720672dfb0ca46f75a8cb1bdd0fab1b 100644 --- a/src/views/SelectProfileView.vue +++ b/src/views/SelectProfileView.vue @@ -13,7 +13,7 @@ computed: { ...mapState(useAuthStore, ['profiles']) - }, + }, methods: { // Sends the user into the home page logged in as the profile they clicked on diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..8e5e024b1585d6d88f8cd29a97dfb6454e783885 --- /dev/null +++ b/src/views/SettingsView.vue @@ -0,0 +1,138 @@ +<template> + <main> + <EditProfile></EditProfile> + <EditAccount></EditAccount> + <div id = "attribution "> + <h3>Om ikonene brukt i denne appen</h3> + <a href="https://www.flaticon.com/free-icons/leaf" title="leaf icons">Leaf icons created by Freepik - Flaticon</a> + <p>Icons by <a href="https://iconify.design/">Iconify</a></p> + </div> + </main> +</template> +<script setup> + +import EditProfile from "../components/EditProfile.vue"; +import EditAccount from "../components/EditAccount.vue"; +</script> + + +<script> +import {mapStores} from "pinia"; +import {Icon} from "@iconify/vue"; +import {API} from "@/util/API"; +import { useAuthStore } from "@/stores/authStore"; +import router from "../router"; +export default { + name: "ProfileSettings", + components: {Icon}, + computed: { + ...mapStores(useAuthStore), + iconColor() { + return "#000000" + }, + }, + beforeMount() { + if (!useAuthStore().isLoggedIn) { + router.push('/login') + } + }, +} +</script> + +<style scoped lang ="scss"> + + +main { + background-color: white; + color:black; + display:flex; + justify-content: center; + align-items: center; + flex-direction: column; + width: 100%; + text-align: left; + left:0; +} + +#profilepicture-container { + display:flex; + border-radius:50%; + width:100px; + height: 100px; + background-color: white; + justify-content: center; + align-items: center; + border: 3px solid base.$grey; +} + +img { + border-radius: 50%; +} + +#changeUserImage { + display:flex; +} + +#changeUserImage #profilepicture-container { + width: 50px; + height: 50px; +} + +.infoText { + background-color: white; + padding: .5em; + margin: .4em; +} + +form { + background-color: base.$grey; + color: black; + align-content: end; + padding: 2em; + margin-top: 2em; + margin-bottom: 2em; +} + +input[type="text"], +input[type="password"]{ + width: 100%; + padding: .5em; +} +#submitbuttonBox { + display:flex; + justify-content: space-between; +} +button { + background-color: base.$red; + color: black; + + border: 1px solid black; + + margin: 1em; +} + + +#changeUserBtn { + padding:.9em; + +} +button:hover{ + background-color: #282828; + + } + +.saveBtn, .delBtn { + background-color: base.$green; + color: white; + font-weight: bold; + padding:.9em; + border:none; +} +.delBtn { + background-color: darkred; +} + +#dangerZone { + color: darkred; +} +</style> \ No newline at end of file