Skip to content
Snippets Groups Projects
Commit f84a080f authored by Ingrid's avatar Ingrid
Browse files

merge with main

parents d445df8e 39847f2e
No related branches found
No related tags found
1 merge request!13Fridge view
Showing
with 598 additions and 551 deletions
.env 0 → 100644
VITE_BACKEND_URL = "http://localhost:8080"
\ No newline at end of file
image: node:latest image: cypress/base:latest
stages: stages:
- build - build
...@@ -24,3 +24,8 @@ unit-test-job: # This job runs in the test stage. ...@@ -24,3 +24,8 @@ unit-test-job: # This job runs in the test stage.
stage: test # It only starts when the job in the build stage completes successfully. stage: test # It only starts when the job in the build stage completes successfully.
script: script:
- npm run test:unit - npm run test:unit
e2e-test-job: # This job runs in the test stage.
stage: test # It only starts when the job in the build stage completes successfully.
script:
- npm run test:e2e:dev
describe('Login fails with wrong credentials', () => {
it('passes', () => {
cy.visit('http://localhost:4173/login')
cy.get('#login-button').trigger('click')
cy.get('#error-message').contains("Kunne ikke logge inn!")
cy.get('#email-input').type('en bruker som ikke finnes')
cy.get('#login-button').trigger('click')
cy.get('#error-message').contains("Kunne ikke logge inn!")
cy.get('#password-input').type('hei')
cy.get('#login-button').trigger('click')
cy.get('#error-message').contains("Kunne ikke logge inn!")
})
})
\ No newline at end of file
describe('Cannot register with wrong fields', () => {
it('passes', () => {
cy.visit('http://localhost:4173/registerAccount')
cy.get('#register-button').trigger('click')
cy.get('#error-message').contains("Vennligst fyll inn alle feltene")
cy.get('#email-input').type('en ugyldig epost')
cy.get('#register-button').trigger('click')
cy.get('#error-message').contains("Vennligst fyll inn alle feltene")
cy.get('#password-input').type('hei')
cy.get('#register-button').trigger('click')
cy.get('#error-message').contains("Vennligst fyll inn alle feltene")
})
})
\ No newline at end of file
This diff is collapsed.
...@@ -8,21 +8,17 @@ ...@@ -8,21 +8,17 @@
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest --environment jsdom --root src/", "test:unit": "vitest --environment jsdom --root src/",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'", "test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'" "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress run --e2e'"
}, },
"dependencies": { "dependencies": {
"@iconify/iconify": "^3.1.0",
"axios": "^1.3.6",
"pinia": "^2.0.28", "pinia": "^2.0.28",
"sass": "^1.62.0",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@iconify/vue": "^4.1.1",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vue/test-utils": "^2.2.6", "@vue/test-utils": "^2.2.6",
"cypress": "^12.10.0", "cypress": "^12.0.2",
"jsdom": "^20.0.3", "jsdom": "^20.0.3",
"node-sass": "^8.0.0", "node-sass": "^8.0.0",
"sass-loader": "^13.2.2", "sass-loader": "^13.2.2",
......
<script setup> <script setup>
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import Navbar from "@/components/Navbar.vue"; import Navbar from "@/components/Navbar.vue";
</script> </script>
<template> <template>
...@@ -47,7 +49,7 @@ nav a:first-of-type { ...@@ -47,7 +49,7 @@ nav a:first-of-type {
@media (min-width: 1024px) { @media (min-width: 1024px) {
header { header {
display: flex; display: flex;
qplace-items: center; place-items: center;
padding-right: calc(var(--section-gap) / 2); padding-right: calc(var(--section-gap) / 2);
} }
......
...@@ -36,19 +36,6 @@ ...@@ -36,19 +36,6 @@
--section-gap: 160px; --section-gap: 160px;
} }
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*, *,
*::before, *::before,
......
...@@ -29,7 +29,6 @@ a, ...@@ -29,7 +29,6 @@ a,
#app { #app {
display: grid; display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem; padding: 0 2rem;
} }
} }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
</RouterLink> </RouterLink>
</li> </li>
<li> <li>
<RouterLink :to="'/myFridge'" :aria-label="'link to fridge page'"> <RouterLink :to="'/'" :aria-label="'link to fridge page'">
<Icon icon="mingcute:fridge-line" :color="iconColor" :style="{ fontSize: iconSize }"/> <Icon icon="mingcute:fridge-line" :color="iconColor" :style="{ fontSize: iconSize }"/>
</RouterLink> </RouterLink>
</li> </li>
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
<div id = "logoContainer" :style="{ width: logoSize, height: logoSize }" > <div id = "logoContainer" :style="{ width: logoSize, height: logoSize }" >
<img src="../components/icons/logo.png" alt="logo"> <img src="../components/icons/logo.png" alt="logo">
</div> </div>
<!--<Icon icon="mdi:leaf-circle" :color="iconColor" :style="{ fontSize: logoSize }" />-->
</RouterLink> </RouterLink>
</li> </li>
<li> <li>
...@@ -49,7 +48,7 @@ export default { ...@@ -49,7 +48,7 @@ export default {
return `32px`; return `32px`;
}, },
logoSize() { logoSize() {
return '62px'; return '52px';
} }
} }
} }
...@@ -63,8 +62,6 @@ export default { ...@@ -63,8 +62,6 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items:center; align-items:center;
} }
#logoContainer img{ #logoContainer img{
width:70%; width:70%;
...@@ -78,7 +75,7 @@ nav { ...@@ -78,7 +75,7 @@ nav {
width: 100%; width: 100%;
background-color: base.$green; background-color: base.$green;
margin:0; margin:0;
padding:0; padding:5px;
} }
ul { ul {
...@@ -91,8 +88,6 @@ ul { ...@@ -91,8 +88,6 @@ ul {
margin-left: 1em; margin-left: 1em;
} }
@media only screen and (min-width: base.$desktop-min){ @media only screen and (min-width: base.$desktop-min){
nav { nav {
top:0; top:0;
...@@ -104,5 +99,4 @@ ul { ...@@ -104,5 +99,4 @@ ul {
li{ li{
text-align: center; text-align: center;
} }
</style> </style>
\ No newline at end of file
...@@ -9,8 +9,8 @@ describe('Navbar', () => { ...@@ -9,8 +9,8 @@ describe('Navbar', () => {
expect(wrapper.findAll('RouterLink').length).toBe(5) expect(wrapper.findAll('RouterLink').length).toBe(5)
}) })
it('contains 5 icons', () => { it('contains 4 icons', () => {
const wrapper = mount(Navbar) const wrapper = mount(Navbar)
const icons = wrapper.findAllComponents(Icon) const icons = wrapper.findAllComponents(Icon)
expect(icons).toHaveLength(5)}) expect(icons).toHaveLength(4)})
}) })
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedState from "pinia-plugin-persistedstate";
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
...@@ -8,7 +9,10 @@ import './assets/main.css' ...@@ -8,7 +9,10 @@ import './assets/main.css'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) const pinia = createPinia();
pinia.use(piniaPluginPersistedState);
app.use(pinia)
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from "@/views/HomeView.vue"; import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import SelectProfileView from '../views/SelectProfileView.vue'
import RegisterAccountView from '../views/RegisterAccountView.vue'
import FridgeView from "@/views/FridgeView.vue"; import FridgeView from "@/views/FridgeView.vue";
...@@ -12,6 +16,20 @@ const router = createRouter({ ...@@ -12,6 +16,20 @@ const router = createRouter({
component: HomeView component: HomeView
}, },
{ {
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/selectProfile',
name: "selectProfile",
component: SelectProfileView
},
{
path: '/registerAccount',
name: 'registerAccount',
component: RegisterAccountView
},{
path: '/myFridge', path: '/myFridge',
name: 'myFridge', name: 'myFridge',
component: FridgeView component: FridgeView
......
import { defineStore } from "pinia";
export const useAuthStore = defineStore("auth", {
state: () => {
return {
token: "",
account: {},
profile: {},
profiles: []
};
},
persist: {
storage: localStorage
},
getters: {
isLoggedIn() {
return this.token.length > 0
}
},
actions: {
setToken(token) {
this.token = token;
},
setAccount(account) {
this.account = account;
},
logout() {
this.$reset();
},
setProfile(profile) {
this.profile = profile;
},
setProfiles(profiles) {
this.profiles = profiles;
}
}
});
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
/*APP COLORS*/ /*APP COLORS*/
$green: #00663C; $green: #00663C;
$light-green: hsla(160, 100%, 37%, 1);
$white:#FFFFFF; $white:#FFFFFF;
$grey:#D9D9D9; $grey:#D9D9D9;
$red:#EE6D6D; $red:#EE6D6D;
......
import axios from "axios"; import axios from "axios";
import { useAuthStore } from "@/stores/authStore.js";
import jwt_decode from "jwt-decode";
import router from "@/router/index";
export const API = {
/**
* API method to send a login request.
* If login succeeds, the logged in User and their token
* is saved to the Pinia AuthStore
*
* @param email email address of the user to log in as
* @param password password to log in with
* @returns a Result with whether the login attempt succeeded
*/
login: async (request) => {
const authStore = useAuthStore();
let token;
authStore.logout(); //in case someone for some reason is logged in
return axios.post(
`${import.meta.env.VITE_BACKEND_URL}/login`,
request,
)
.then(async (response) => {
token = response.data;
const id = (jwt_decode(token)).id;
return API.getAccount(id, token)
.then((user) => {
authStore.setAccount(user);
authStore.setToken(token);
API.getProfiles()
.then(response => {authStore.setProfiles(response)})
.catch(() => {throw new Error()})
return;
})
.catch(() => {
throw new Error();
});
})
.catch(() => {
throw new Error();
});
},
/**
* API method to get a account by their ID
* @param id ID number of the account to retrieve
* @returns A promise that resolves to a User if the API call succeeds,
* or is rejected if the API call fails
*/
getAccount: async (id, token) => {
return axios.get(`${import.meta.env.VITE_BACKEND_URL}/account/${id}`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((response) => {
return response.data;
})
.catch(() => {
throw new Error("Account not found or not accessible");
});
},
// Sends the user into the home page logged in as the profile they clicked on
selectProfile: async (id) => {
const authStore = useAuthStore()
return axios.get(`${import.meta.env.VITE_BACKEND_URL}/profile/${id}`, {
headers: { Authorization: `Bearer ${authStore.token}` },
})
.then((response) => {
authStore.setProfile(response.data)
router.push("/")
})
.catch(() => {
throw new Error("Profile not found or not accessible")
})
},
// Sends the user into the "register profile" view
addProfile: async () => {
console.log("todo");
},
// Returns all profiles to the logged in user
getProfiles: async () => {
const authStore = useAuthStore();
if (!authStore.isLoggedIn) {
throw new Error();
}
return axios.get(import.meta.env.VITE_BACKEND_URL + '/profile', {
headers: { Authorization: "Bearer " + authStore.token },
},
)
.then(response => {
return response.data
}).catch(() => {
throw new Error();
});
},
// Registers a new account and logs into it
addAccount: async (request) => {
const authStore = useAuthStore();
axios.post(import.meta.env.VITE_BACKEND_URL + '/account', request)
.then(() => {
API.login({email: request.email, password: request.password})
.catch(err => {console.log(err)})
})
.catch(() => {throw new Error()})
}
}
export const API = { export const API = {
/** /**
* Fetches all fridgeItems * Fetches all fridgeItems
......
<script setup> <script>
import { useAuthStore } from "@/stores/authStore.js";
import { mapState } from 'pinia'
export default {
data() {
return {
}
},
computed: {
...mapState(useAuthStore, ['profile'])
}
}
</script> </script>
<template> <template>
<main> <main>
<p>HALLO</p> <div class="content">
<div class="welcome">
<img id="logo" src="../components/icons/logo.png" alt="Logo">
<h1>Velkommen, {{ this.profile.name }}!</h1>
</div>
<div class="gamification">
</div>
<div class="tips">
<img id="tips-img" src="../components/icons/logo.png" alt="Logo">
<p id="tips-text">Her kommer tips du kanskje kan ha nytte av, trykk på meg for å gå til neste tips!</p>
</div>
</div>
</main> </main>
</template> </template>
<style scoped>
.container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 300px;
margin-top: 40px;
}
.welcome {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 20px;
}
#logo {
width: 75px;
height: 75px;
}
#tips-img {
width: 40px;
height: 40px;
margin: auto 0;
}
.tips {
display:flex;
padding: 10px;
min-height: 75px;
max-height: 250px;
background-color: rgb(232, 232, 232);
margin-left: 10px;
margin-right: 10px;
}
#tips-text {
overflow: hidden;
padding-left: 10px;
}
@media (min-width: 1024px) {
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
}
</style>
<script>
import { API } from '@/util/API.js';
import router from '@/router/index.js';
export default {
data() {
return {
welcomemsg: "Velkommen tilbake",
email: "",
password: "",
errormsg: "",
}
},
methods: {
login() {
//todo: implement when API is up
API.login({email: this.email, password: this.password}).then(() => {
router.push("/selectProfile");
})
.catch(() => {
this.errormsg = "Kunne ikke logge inn! Sjekk brukernavn og passord, og prøv igjen";
});
}
}
}
</script>
<template>
<main>
<div class="login-container">
<img id="logo" src="../components/icons/logo.png" alt="Logo">
<h1>{{ welcomemsg }}</h1>
<form @submit.prevent>
<div class="field-container">
<label for="email">E-post</label>
<input id="email-input" name="email" type="text" v-model="email" />
</div>
<div class="field-container">
<label for="password">Passord</label>
<input id="password-input" name="password" type="password" v-model="password" />
</div>
<p id="error-message">{{ errormsg }}</p>
<button @click="login" id="login-button">Logg inn</button>
</form>
<p><RouterLink to="/registerAccount">Ny bruker</RouterLink> - <a href="#">Glemt passord?</a></p>
</div>
</main>
</template>
<style lang="scss" scoped>
.login-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 300px;
margin-top: 40px;
}
form {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.field-container {
padding: 10px;
display: flex;
flex-direction: column;
}
input {
height: 40px;
font-size: 16px;
padding-left: 10px;
}
label {
font-size: 18px;
}
#login-button {
background-color: base.$green;
color: #FFFFFF;
border-radius: 5px;
border-style: none;
width: 150px;
height: 40px;
font-size: 18px;
font-weight: bold;
margin: 20px;
}
#login-button:hover {
background-color: base.$light-green;
}
#logo {
width: 100px;
height: 100px;
}
@media (min-width: 1024px) {
.login-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
}
</style>
<template>
<div class="content">
<img id="logo" src="../components/icons/logo.png" alt="Logo">
<h1>Registrer konto</h1>
<form @submit.prevent>
<div class="field-container">
<label for="name">Navn</label>
<input id="input" name="name" type="text" v-model="name" />
</div>
<div class="field-container">
<label for="email">E-post</label>
<input id="email-input" name="email" type="email" v-model="email" />
</div>
<div class="field-container">
<label for="password">Passord</label>
<input id="password-input" name="password" type="password" v-model="password" />
</div>
<p id="error-message">{{ errormsg }}</p>
<button @click="register" id="register-button">Registrer</button>
</form>
</div>
</template>
<script>
import { API } from '@/util/API.js';
import router from '@/router/index.js';
export default {
data() {
return {
email: "",
name: "",
password: "",
errormsg: ""
}
},
methods: {
register() {
if (this.name === "" || this.password === "") {
this.errormsg = "Vennligst fyll inn alle feltene"
}
else {
API.addAccount({firstname: this.name, email: this.email, password: this.password}).then(() => {
router.push("/selectProfile")
})
.catch(() => {
throw new Error()
});
}
}
}
}
</script>
<style scoped>
.content {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 300px;
margin-top: 40px;
}
form {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.field-container {
padding: 10px;
display: flex;
flex-direction: column;
}
input {
height: 40px;
font-size: 16px;
padding-left: 10px;
}
label {
font-size: 18px;
}
#register-button {
background-color: #00663C;
color: #FFFFFF;
border-radius: 5px;
border-style: none;
width: 150px;
height: 40px;
font-size: 18px;
font-weight: bold;
margin: 20px;
}
#register-button:hover {
background-color: #04be80;
}
#logo {
width: 100px;
height: 100px;
}
@media (min-width: 1024px) {
.login-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
}
</style>
\ No newline at end of file
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