diff --git a/public/profile/default_avatar.png b/public/profile/default_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..45afcfe8037e158b9ab75b9e7219692e50084770 Binary files /dev/null and b/public/profile/default_avatar.png differ diff --git a/src/components/EditAccount.vue b/src/components/EditAccount.vue index 15a6da8243327b8d2c7ffe517df37ff8f6d66c80..7ac577383f77346d52f6e6f3b959815e08b85e8c 100644 --- a/src/components/EditAccount.vue +++ b/src/components/EditAccount.vue @@ -1,7 +1,6 @@ <template> <h1>Konto-innstillinger</h1> <form @submit.prevent="submit"> - <p class="infoText">OBS: Kontakt admin dersom du ønsker å oppdatere epost</p><br> <p>Epost: {{this.account.email}}</p><br> @@ -13,8 +12,9 @@ <input type="password" id="password" v-model="updatedAccount.upPassword"> <button class="saveBtn" @click="saveAccountSettings">Lagre profilendringer</button> - + <p :style={color:alertMsgColor} id="alert">{{alertMsg}}</p> </form> + <br> <br> <hr> @@ -25,6 +25,7 @@ <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="delBtn" @click="deleteAccount">SLETT KONTO</button> + <p :style={color:alertMsgColor} id="alert">{{delAlertMsg}}</p> </form> </template> @@ -53,6 +54,9 @@ export default { data() { return { deletionConfirmation: false, + alertMsg:'', + alertMsgColor:'black', + delAlertMsg:'', } }, methods: { @@ -67,8 +71,9 @@ export default { } ).then((savedAccount)=>{ useAuthStore().setAccount(savedAccount); - alert("Bruker oppdatert.") + this.setAlertText("Melding: Bruker oppdatert.",'light-green'); }).catch((error)=> { + this.setAlertText("Melding: Det oppsto en feil. ",'red'); console.log(error) }) } else { @@ -81,15 +86,16 @@ export default { } ).then((savedAccount)=>{ useAuthStore().setAccount(savedAccount); - alert("Konto oppdatert.") + this.setAlertText("Melding: Konto oppdatert.",'light-green') }).catch((error)=> { + this.setAlertText("Melding: Det oppsto en feil. ",'red'); console.log(error) }) } }, deleteAccount(){ if(this.deletionConfirmation===false){ - alert("Du må bekrefte at du vil slette konto ved å huke av boksen") + this.setDeleationAlertText("Du må bekrefte at du vil slette konto ved å huke av boksen", 'red') } else { const id = this.account.id; @@ -98,11 +104,43 @@ export default { ).then(()=>{ router.push('/login') }).catch((error)=> { - alert("Det oppsto en feil ved sletting av bruker") - console.log(error) + this.setDeleationAlertText("Melding: Det oppsto en feil ved sletting av bruker", 'red') }) } }, + setAlertText(text, color){ + switch (color) { + case 'red': + this.alertMsgColor ='#EE6D6D'; + this.alertMsg = text; + break; + case 'light-green': + this.alertMsgColor ='hsla(160, 100%, 37%, 1);'; + this.alertMsg = text; + break; + default: + this.alertMsgColor ='black'; + this.alertMsg = text; + break; + } + }, + setDeleationAlertText(text,color){ + switch (color) { + case 'red': + this.alertMsgColor ='#EE6D6D'; + this.delAlertMsg = text; + break; + case 'light-green': + this.alertMsgColor ='hsla(160, 100%, 37%, 1);'; + this.delAlertMsg = text; + break; + default: + this.alertMsgColor ='black'; + this.delAlertMsg = text; + break; + } + } + } } </script> @@ -142,6 +180,7 @@ input[type="password"]{ button { background-color: base.$red; + color: black; border: 1px solid black; @@ -168,4 +207,12 @@ button:hover{ color: darkred; } +#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/EditProfile.vue b/src/components/EditProfile.vue index 0dd9a849128c297c41f2aac684d764e55e1d6d01..20d746a34266257e8d6996913324ae0a8972de61 100644 --- a/src/components/EditProfile.vue +++ b/src/components/EditProfile.vue @@ -2,7 +2,7 @@ <h1><br><br>Profilinnstillinger <br></h1> <div v-if="hasProfileImage" id = "profilepicture-container"> - <img width="100" :src="getProfileImage" alt="profile picture"> + <img width="100" :src="this.updatedProfile.upImage" alt="profile picture"> </div> <div v-else id = "profilepicture-container"> <Icon icon="material-symbols:person" :color=iconColor :style="{ fontSize: '500px'}" /> @@ -42,6 +42,8 @@ <button class="delBtn" @click="deleteProfile">Slett brukerprofil</button> </div> + <p :style={color:alertMsgColor} id="alert">{{alertMsg}}</p> + </form> </template> @@ -55,6 +57,12 @@ import router from "../router"; export default { name: "EditProfile", components: {Icon}, + data() { + return { + alertMsg:'', + alertMsgColor:'black', + } + }, computed: { ...mapState(useAuthStore, ['profile']), @@ -71,9 +79,6 @@ export default { }, hasProfileImage() { return this.updatedProfile.upImage.length > 0; - }, - getProfileImage(){ - return this.updatedProfile.upImage; } }, methods: { @@ -81,9 +86,8 @@ export default { const id = this.profile.id; const numOfProfiles = API.getProfiles().length - console.log("antall profiler: " + numOfProfiles) //todo - if(numOfProfiles===1 && this.updatedProfile.upRestricted==true){ - alert("Du må ha minst en standardprofil per konto. (ingen endringer er gjort)") + if(numOfProfiles===1 && this.updatedProfile.upRestricted===true){ + this.setAlertText("Du må ha minst en standardprofil per konto. (ingen endringer er gjort)",'red') } else { API.updateProfile( @@ -94,14 +98,15 @@ export default { } ).then((savedProfile)=>{ useAuthStore().setProfile(savedProfile); - alert("profil oppdatert.") + this.setAlertText("profil oppdatert.",'light-green') }).catch((error)=> { + this.setAlertText("Det oppsto en feil",'red') console.log(error) }) } }, chooseProfilePicture(){ - alert("skriv inn bildelenke i feltet, og oppdater innstillinger") + this.setAlertText("skriv inn bildelenke i feltet, og oppdater innstillinger",'black') }, changeProfile(){ router.push("/selectProfile"); @@ -109,16 +114,32 @@ export default { deleteProfile(){ const numOfProfiles = API.getProfiles().length if(numOfProfiles===1){ - alert("Du kan ikke slette profilen. Hver Konto må ha minst en profil") + this.setAlertText("Du kan ikke slette profilen. Hver Konto må ha minst en profil",'red') }else { const id = this.profile.id; API.deleteProfile(id).then(()=>{ router.push('/selectProfile') }).catch((error)=> { - alert("Det oppsto en feil ved sletting profil: " + error) + this.setAlertText("Det oppsto en feil ved sletting profil", 'red') }) } - } + }, + setAlertText(text, color){ + switch (color) { + case 'red': + this.alertMsgColor ='#EE6D6D'; + this.alertMsg = text; + break; + case 'light-green': + this.alertMsgColor ='hsla(160, 100%, 37%, 1);'; + this.alertMsg = text; + break; + default: + this.alertMsgColor ='black'; + this.alertMsg = text; + break; + } + }, } } @@ -219,5 +240,13 @@ button:hover{ #dangerZone { color: darkred; } +#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/Navbar.vue b/src/components/Navbar.vue index e3a5230d4ff23026c079780c5ba1a05bedf24139..131108c761a8683cd36279b1e2176a3e943515fa 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -1,6 +1,6 @@ <template> <div> - <nav v-if="isLoggedIn"> + <nav> <ul :aria-label = "'Navigation links'"> <li> <RouterLink :to="'/'" :aria-label="'link to home page'"> @@ -13,8 +13,8 @@ </RouterLink> </li> <li> - <RouterLink :to="'/'" :aria-label="'link to fridge page'"> - <Icon icon="ic:baseline-calendar-month" :color="iconColor" :style="{ fontSize: logoSize }"/> + <RouterLink :to="'/'" :aria-label="'link to week menu'"> + <Icon icon="ic:baseline-calendar-month" :color="iconColor" :style="{ fontSize: iconSize }"/> </RouterLink> </li> <li> @@ -34,7 +34,6 @@ <script> import { Icon } from '@iconify/vue'; import Logo from "@/components/Logo.vue"; -import {useAuthStore} from "@/stores/authStore"; export default { name: "Navbar", @@ -47,14 +46,8 @@ export default { return `32px`; }, logoSize() { - return '45px'; - }, - logocolor() { - return '#00663C' - }, - isLoggedIn(){ - return useAuthStore().isLoggedIn; - } + return '52px'; + } } } </script> @@ -68,7 +61,7 @@ export default { justify-content: center; align-items:center; } -#logoContainer Icon{ +#logoContainer img{ width:70%; } @@ -80,7 +73,9 @@ nav { width: 100%; background-color: base.$green; margin:0; - padding:5px; + qpadding:5px; + align-items: center; + } ul { @@ -89,8 +84,12 @@ ul { justify-content: space-between; align-items: center; padding: 0; + margin-top: .4em; margin-right: 1em; margin-left: 1em; + + + } @media only screen and (min-width: base.$desktop-min){ diff --git a/src/components/Toggle.vue b/src/components/Toggle.vue new file mode 100644 index 0000000000000000000000000000000000000000..adb000f67767329aa2df79ffc90da1a880e468d5 --- /dev/null +++ b/src/components/Toggle.vue @@ -0,0 +1,88 @@ +<script> +export default { + data: () => { + return { + state: false, + } + }, + emits: ['toggled'], + methods: { + emitUpdate() { + this.state = !this.state; + this.$emit('toggled'); + } + } +} +</script> + +<template> + <label class="toggle"> + <input type="checkbox" @change="emitUpdate"> + <span class="toggle_slider"></span> + </label> +</template> + +<style scoped lang="scss"> +$width: 40px; +$height: calc(2 * $width / 3); + +$on-color: green; +$off-color: #cccccc; + +.toggle { + position: relative; + display: inline-block; + + border-radius: calc($height / 2); + + width: $width; + height: $height; + + background-color: $off-color; + + input { + display: none; + } +} + +.toggle_slider { + position: absolute; + cursor: pointer; + + border-radius: calc($height / 2); + + top: 0; + left: 0; + right: 0; + bottom: 0; + + background-color: #ccc; + + &:before { + position: absolute; + content: ""; + + border-radius: 50%; + + height: calc(0.8 * $height); + width: calc(0.8 * $height); + left: calc(0.1 * $height); + top: calc(0.1 * $height); + + background-color: white; + transition: 200ms; + } +} + +input:checked+.toggle_slider { + background-color: $on-color; +} + +input:focus+.toggle_slider { + box-shadow: 0 0 1px white; +} + +input:checked+.toggle_slider:before { + transform: translateX(calc($width / 3)); +} +</style> \ No newline at end of file diff --git a/src/components/__tests__/Navbar.spec.js b/src/components/__tests__/Navbar.spec.js index ba70bb53caef585fbb383a2df2e71bede7258f81..a212573ecee960d8a98e37d63d093b3b2b2a2066 100644 --- a/src/components/__tests__/Navbar.spec.js +++ b/src/components/__tests__/Navbar.spec.js @@ -9,8 +9,8 @@ describe('Navbar', () => { expect(wrapper.findAll('RouterLink').length).toBe(5) }) - it('contains 4 icons', () => { + it('contains 5 icons', () => { const wrapper = mount(Navbar) const icons = wrapper.findAllComponents(Icon) - expect(icons).toHaveLength(4)}) + expect(icons).toHaveLength(5)}) }) diff --git a/src/router/index.js b/src/router/index.js index 3e728b8f1fd8fa9a4c4c68a206a78fefe4b1da96..642a5aecd5c1e5b8a3b2999910473c940ab01d51 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -4,6 +4,7 @@ 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' const router = createRouter({ @@ -24,6 +25,11 @@ const router = createRouter({ name: "selectProfile", component: SelectProfileView }, + { + path: '/newProfile', + name: "newProfile", + component: ProfileCreationView + }, { path: '/registerAccount', name: 'registerAccount', diff --git a/src/util/API.js b/src/util/API.js index a9c0fed48eb1b24ef2ddb52c62a78b85f74edab6..43396b8210de1ed199dcfedc72eb69a9b97d91cc 100644 --- a/src/util/API.js +++ b/src/util/API.js @@ -82,9 +82,28 @@ export const API = { }, - // Sends the user into the "register profile" view - addProfile: async () => { - console.log("todo"); + /** + * Sends a request to create a new profile on the currently logged in account + * + * @typedef {{name: string, profileImageUrl: string, isRestricted: boolean}} ProfileType + * @param {ProfileType} profile + * @returns + */ + addProfile: async (profile) => { + const authStore = useAuthStore(); + if (!authStore.isLoggedIn) { + throw new Error(); + } + + return axios.post(import.meta.env.VITE_BACKEND_URL + '/profile', { + headers: { Authorization: "Bearer " + authStore.token }, + body: profile + }) + .then((response) => { + return response.data; + }).catch(() => { + throw new Error(); + }) }, // Returns all profiles to the logged in user diff --git a/src/views/ProfileCreationView.vue b/src/views/ProfileCreationView.vue new file mode 100644 index 0000000000000000000000000000000000000000..22cb341b164554dd4416834a341b895504de2bde --- /dev/null +++ b/src/views/ProfileCreationView.vue @@ -0,0 +1,153 @@ +<script> +import Toggle from '../components/Toggle.vue'; +import { API } from '../util/API'; + +export default { + data: () => { + return { + image: "profile/default_avatar.png", + profile: { + profileImageUrl: "", + name: "", + isRestricted: false + } + }; + }, + methods: { + submit() { + this.profile.isRestricted = this.$refs.toggle.state; + API.addProfile(this.profile); + }, + updateImg() { + let file = document.getElementById("avatar").files[0]; + let reader = new FileReader(); + reader.onload = function (ev) { + document.getElementById("avatar_preview").src = ev.target.result; + }; + reader.readAsDataURL(file); + } + }, + components: { Toggle } +} +</script> + +<template> + <main> + <h1>Ny profil</h1> + + <form action="todo" method="post"> + <label for="avatar" id="avatar_label"> + <div class="img_hover"> + <img :src="image" alt="fjes" id="avatar_preview"> + <p class="hover_text"> + Klikk for å endre + </p> + </div> + </label> + <input type="file" name="avatar" id="avatar" @change="updateImg"> + <label for="name">Navn</label> + <input name="name" type="text" v-model="profile.name"> + <div class="check_container"> + <label for="child">Begrenset:</label> + <Toggle ref="toggle" /> + </div> + </form> + <button @click="submit">Opprett</button> + </main> +</template> + +<style scoped lang="scss"> +@import '../style.scss'; + +$gap: 1em; + +$img-size: 150px; + +input[type="file"] { + display: none; +} + +main { + display: flex; + flex-direction: column; + + padding-top: 15%; + + flex-wrap: wrap; + justify-content: center; + align-items: center; + + gap: $gap; + + form { + gap: $gap; + } +} + +#avatar_label { + display: flex; + justify-content: center; + width: 100%; +} + +.check_container { + display: flex; + gap: .5em; +} + +.img_hover { + display: flex; + + justify-content: center; + align-items: center; + + &:hover .hover_text { + visibility: visible; + opacity: 1; + } + + &:hover img { + filter: brightness(70%); + border-radius: 25%; + } + + img { + height: $img-size; + width: $img-size; + border-radius: 50%; + + object-fit: cover; + + transition: all 300ms ease; + } +} + +.hover_text { + position: absolute; + + font-weight: bold; + + color: #fff; + visibility: hidden; + opacity: 0; + + transition: all 350ms eases; +} + +form { + display: flex; + flex-direction: column; + + max-width: 20em; +} + +button { + padding: .5rem 1rem; + border: 2px $green solid; + border-radius: .5rem; + background-color: $light-green; + + color: white; + font-weight: bold; +} +</style> \ No newline at end of file