Skip to content
Snippets Groups Projects
Commit edd25920 authored by Jakob Karevold Grønhaug's avatar Jakob Karevold Grønhaug
Browse files

Merge branch 'main' into 'ukemeny'

# Conflicts:
#   src/router/index.js
#   src/style.scss
parents 600d4532 b1284681
No related branches found
No related tags found
1 merge request!22ukemeny
Pipeline #225434 failed
Showing
with 877 additions and 94 deletions
describe('Correct navigation links', () => {
/*TODO*/
})
describe('Navbar on all pages', () => {
/*TODO*/
})
\ No newline at end of file
})
......@@ -13,6 +13,7 @@
"jwt-decode": "^3.1.2",
"pinia": "^2.0.35",
"pinia-plugin-persistedstate": "^3.1.0",
"sass": "^1.62.0",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
......@@ -1444,9 +1445,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
......@@ -1653,9 +1651,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=8"
}
......@@ -1691,9 +1686,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"fill-range": "^7.0.1"
},
......@@ -2088,15 +2080,12 @@
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"optional": true,
"peer": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
......@@ -3042,9 +3031,6 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
......@@ -3164,7 +3150,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
......@@ -3308,9 +3293,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"is-glob": "^4.0.1"
},
......@@ -3598,10 +3580,7 @@
"node_modules/immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"dev": true,
"optional": true,
"peer": true
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
},
"node_modules/imurmurhash": {
"version": "0.1.4",
......@@ -3668,9 +3647,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
......@@ -3706,9 +3682,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
......@@ -3726,9 +3699,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
......@@ -3762,9 +3732,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.12.0"
}
......@@ -5074,9 +5041,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
......@@ -5329,9 +5293,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=8.6"
},
......@@ -5715,9 +5676,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"picomatch": "^2.2.1"
},
......@@ -5919,9 +5877,6 @@
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz",
"integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
......@@ -6799,9 +6754,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"is-number": "^7.0.0"
},
......
......@@ -17,6 +17,7 @@
"jwt-decode": "^3.1.2",
"pinia": "^2.0.35",
"pinia-plugin-persistedstate": "^3.1.0",
"sass": "^1.62.0",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
......
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import Navbar from "@/components/Navbar.vue";
import { RouterView } from 'vue-router'
import Navbar from "@/components/Navbar.vue";
</script>
<template>
<Navbar></Navbar>
<body>
<RouterView />
</body>
<Navbar v-if="$route.name !== 'login' && $route.name !== 'selectProfile'" />
<RouterView />
</template>
<style scoped>
<style lang=scss scoped>
header {
line-height: 1.5;
max-height: 100vh;
......@@ -52,7 +48,7 @@ nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
@media (min-width: base.$desktop-min) {
header {
display: flex;
place-items: center;
......
......@@ -25,10 +25,14 @@ a,
body {
display: flex;
place-items: center;
justify-content: center;
align-items: center;
}
#app {
display: grid;
padding: 0 2rem;
padding: 2rem 2rem;
}
}
<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
<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
......@@ -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">
......@@ -10,7 +12,6 @@
</div>
</div>
<hr>
</template>
<script>
......@@ -36,7 +37,7 @@ export default {
}
},
getImage(){
return this.actualItem.item.image_url;
return this.fridgeItem.item.image_url;
},
expirationText() {
......@@ -66,11 +67,7 @@ export default {
}
},
props: {
item: {
type:Object,
required: false,
},
actualItem: {
fridgeItem: {
type: Object,
required:false,
},
......@@ -82,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)
......@@ -94,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();
......@@ -112,13 +109,17 @@ export default {
#item {
background-color: base.$white;
color: black;
qborder-radius: 10px;
padding: 1em;
padding-left: 2em;
padding-right:2em;
display:flex;
align-items: center;
justify-content: space-between;
border-bottom: solid 1px base.$grey;
}
#item:last-child {
border-bottom: none;
}
#fridgeItemName {
......
<template>
<div>
<h2>Tilbudaviser</h2>
<h2>Lenker til tilbudsaviser</h2>
</div>
<div id="list">
......
<template>
<div v-if="showSearch" id="wrapper">
<h3>SØK ETTER VARE</h3>
<h3>Søk etter vare</h3>
<div id="searchBoxDiv">
<input type="text" id="searchBox" v-model="itemSearch">
<button @click="search">Søk</button>
<button id="search-button" @click="search">Søk</button>
</div>
<p>Resultater: ({{searchResult.length}})</p>
......@@ -12,9 +12,10 @@
<option v-for ="item in searchResult" :value="item" :key="item.ean">{{item.name}} ({{item.amount.quantity}}{{item.amount.unit}})</option>
</select>
<p>Antall varer: <span v-if="numOfItemsToAdd>1 && selectedItem!=null">(totalt: {{this.totalNumOfItems}} {{selectedItem.amount.unit}})</span></p>
<input type="number" min='1' v-model="numOfItemsToAdd"><br>
<input id="items-input" type="number" min='1' v-model="numOfItemsToAdd"><br>
<button id = "addToFridgeBtn" @click="addToFridge">Legg i kjøleskap</button>
<button v-if="addsToFridge" id = "addToFridgeBtn" @click="addToFridge">Legg i kjøleskap</button>
<button v-else id = "addToFridgeBtn" @click="addToShoppingList">Legg i handleliste</button>
</div>
......@@ -22,10 +23,17 @@
<script>
import {API} from "@/util/API";
import { useAuthStore } from "@/stores/authStore.js";
export default {
name: "itemSearch",
props: {
addsToFridge: {
type: Boolean,
default: false
}
},
data(){
return{
itemSearch:'',
......@@ -55,7 +63,25 @@ export default {
{
"quantity": this.selectedItem.amount.quantity*num,
"unit": this.selectedItem.amount.unit}}
).then(() => this.$emit('itemsAdded',this.selectedItem)).catch((_)=> console.log("No items were added to the fridge"))
).then((_) => {
this.$emit('itemsAdded',this.selectedItem)
}).catch((_)=> console.log("No items were added to the fridge"))
},
async addToShoppingList() {
const num = this.numOfItemsToAdd;
const authStore = useAuthStore();
if (authStore.profile.restricted) {
await API.addSuggestion(this.selectedItem.id, num)
.then((ingredient) => {this.$emit('itemsAdded', ingredient)})
.catch(err => console.log(err));
} else {
await API.addItemToShoppingList(this.selectedItem.id, num)
.then((ingredient) => {
console.log(ingredient)
this.$emit('itemsAdded', ingredient)
}).catch(err => console.log(err));
}
}
}
}
......@@ -74,6 +100,14 @@ select {
}
#items-input {
margin-bottom: 1em;
}
#search-button {
margin-left: 0.5em;
}
#searchBoxDiv {
display:flex;
width:100%;
......@@ -91,14 +125,17 @@ button {
padding: .5em;
background-color: base.$light-green;
border-radius: 5%;
background-color: base.$green;
color: white;
font-weight: bold;
border: 1px solid base.$green;
height: 30px;
}
button:hover {
border: 1px solid base.$grey;
background-color: base.$light-green-hover;
background-color: base.$light-green;
cursor: pointer;
......
......@@ -18,13 +18,13 @@
</RouterLink>
</li>
<li>
<RouterLink :to="'/'" :aria-label="'link to shopping list'">
<RouterLink :to="'/shoppingList'" :aria-label="'link to shopping list'">
<Icon icon="material-symbols:event-list-outline" :color="iconColor" :style="{ fontSize: iconSize }" />
</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>
......
<template>
<div class="content">
<div class="item">
<div class="check">
<input v-model="isChecked" @click="this.updateChecked()" class="checkbox" type="checkbox">
</div>
<p> {{ this.index }}</p>
<div class="item-label">
<label for="checkbox" class="checkbox-label">{{ this.amount }}x {{ this.itemName }}</label>
</div>
</div>
<div class="delete">
<img @click="deleteItem" src="./icons/trash.svg" alt="delete">
</div>
</div>
</template>
<script>
let uuid = 0;
export default {
beforeCreate() {
this.uuid = uuid.toString();
console.log(this.itemName + " + " + this.uuid)
uuid += 1;
},
name: "ShoppingListItem",
props: {
itemName: {
type: String,
default: "",
required: true
},
amount: {
type: Number,
default: 1,
required: false
},
propValue: {
type: Boolean,
default: false,
required: false
},
index: {
type: Number,
required: true
}
},
data() {
return {
isChecked: this.propValue,
uuid: ""
}
},
methods: {
updateChecked() {
this.$emit('updateItem', {id: this.uuid, isChecked: !this.isChecked})
}
}
}
</script>
<style lang="scss" scoped>
.content {
display: flex;
flex-direction: row;
gap: 10px;
justify-content: space-between;
background-color: white;
padding: 5px;
border-top-right-radius: 10px;
border-top-left-radius: 10px;
//border-bottom: base.$grey solid 1px;
}
.content:not(:first-child) {
border-radius: 0px;
}
.content:last-child {
border-radius: 0px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.content:not(:last-child)::after {
content: '';
height: 1px; /* this works like a border-width */
width: 70%; /* percentage of border shown */
background: base.$grey; /* the color of border */
position: absolute;
bottom: 0;
margin: 0 auto; left: 0; right: 0; /* horizontal centering */
}
.item {
display: flex;
flex-direction: row;
gap: 10px;
}
input[type="checkbox"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #ccc;
}
input[type="checkbox"]:checked {
background-color: base.$light-green;
}
input[type="checkbox"]:checked:after {
content: "\2713"; /* Unicode code for checkmark symbol */
font-size: 24px;
font-weight: bold;
color: white;
text-align: center;
line-height: 35px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
label {
font-size: 20px;
}
img {
width: 30px;
height: 30px;
padding: 5px;
max-height: 100%;
max-width: 100%;
}
.check {
display: flex;
justify-content: center;
align-items: center;
}
.delete {
align-self: flex-end;
}
</style>
\ No newline at end of file
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');
})
})
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
......@@ -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,
},
});
......
import { describe, it, expect, vi} from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import ShoppingListItem from "@/components/ShoppingListItem.vue";
describe('ShoppingListItem', () => {
it('mounts correctly', () => {
const wrapper = mount(ShoppingListItem, {
global: {
plugins: [createTestingPinia({
createSpy: vi.fn,
})],
},
props: {
itemName: "Test",
amount: 99
}
})
expect(wrapper.text()).toMatch("99x Test")
})
})
src/components/icons/tips.png

17.9 KiB

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Outline" viewBox="0 0 24 24" width="512" height="512"><path d="M21,4H17.9A5.009,5.009,0,0,0,13,0H11A5.009,5.009,0,0,0,6.1,4H3A1,1,0,0,0,3,6H4V19a5.006,5.006,0,0,0,5,5h6a5.006,5.006,0,0,0,5-5V6h1a1,1,0,0,0,0-2ZM11,2h2a3.006,3.006,0,0,1,2.829,2H8.171A3.006,3.006,0,0,1,11,2Zm7,17a3,3,0,0,1-3,3H9a3,3,0,0,1-3-3V6H18Z"/><path d="M10,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,10,18Z"/><path d="M14,18a1,1,0,0,0,1-1V11a1,1,0,0,0-2,0v6A1,1,0,0,0,14,18Z"/></svg>
src/components/images/w66XcIlw.jpeg

105 KiB

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'
import ProfileCreationView from '../views/ProfileCreationView.vue'
import RegisterAccountView from '../views/RegisterAccountView.vue'
import PlannerView from '../views/PlannerView.vue'
import PinCodeView from "@/views/PinCodeView.vue";
import FridgeView from "@/views/FridgeView.vue";
import RecipeView from "@/views/RecipeView.vue";
import ShoppingListView from '../views/ShoppingListView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
......@@ -37,6 +41,11 @@ const router = createRouter({
name: 'registerAccount',
component: RegisterAccountView
},
{
path: '/pincode',
name: 'pincode',
component: PinCodeView
},
{
path: '/myFridge',
name: 'myFridge',
......@@ -47,6 +56,26 @@ const router = createRouter({
name: 'planner',
component: PlannerView
},
{
path: '/recipe/:id',
name: 'recipe',
component: RecipeView
},
{
path: '/shoppingList',
name: 'shoppingList',
component: ShoppingListView
},
{
path: '/profileSettings',
name: 'profileSettings',
component: ProfileSettings
},
{
path: '/:catchAll(.*)',
name: "404 Page not found" ,
component: MissingPage
}
]
})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment