diff --git a/.gitignore b/.gitignore index 403adbc1e527906a4aa59558cd582c20bcd1d738..247cd6166cca2da2e9aa9cdf1bdad6338f02b627 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .DS_Store node_modules /dist +coverage # local env files +.env .env.local .env.*.local diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 89d533e50ec3edaf1b3916b80bebdb35ddf9eae8..64226ef4d8cfb17bd806c1510b3063d28b96f959 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,30 +16,41 @@ stages: # List of stages for jobs, and their order of execution - build - test - - deploy -build-job: # This job runs in the build stage, which runs first. +image: node:16 + +cache: + paths: + - node_modules/ + +install_dependencies_job: # This job runs in the build stage, which runs first. stage: build - script: - - echo "Compiling the code..." - - echo "Compile complete." -unit-test-job: # This job runs in the test stage. - stage: test # It only starts when the job in the build stage completes successfully. script: - - echo "Running unit tests... This will take about 60 seconds." - - sleep 60 - - echo "Code coverage is 90%" + - echo "Installing dependencies..." + - npm install + - echo "Dependencies installed." + artifacts: + paths: + - node_modules/ + lint-test-job: # This job also runs in the test stage. stage: test # It can run at the same time as unit-test-job (in parallel). script: - - echo "Linting code... This will take about 10 seconds." - - sleep 10 - - echo "No lint issues found." + - echo "Linting the code..." + - npm run lint + - echo "Code-linting complete." + artifacts: + paths: + - node_modules/ -deploy-job: # This job runs in the deploy stage. - stage: deploy # It only runs when *both* jobs in the test stage complete successfully. +unit-test-job: # This job runs in the test stage. + stage: test # It only starts when the job in the build stage completes successfully. script: - - echo "Deploying application..." - - echo "Application successfully deployed." + - echo "Running unit tests..." + - npm run test:unit -- --coverage + - echo "Unit tests complete." + artifacts: + paths: + - node_modules/ diff --git a/package-lock.json b/package-lock.json index 6c90cbdf471980e235f8a00ec59d5448b42f5298..52e8f82391ad66170f4ffe771040b0cdab18290f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,13 +14,14 @@ "axios": "^0.26.1", "core-js": "^3.8.3", "cssom": "^0.5.0", + "jwt-decode": "^3.1.2", "roboto-fontface": "*", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuelidate": "^0.7.7", "vuex": "^4.0.0", "vuex-persistedstate": "^4.1.0", - "webfontloader": "^1.0.0" + "webfontloader": "^1.6.28" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -40,6 +41,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "jest": "^27.0.5", + "jest-serializer-vue": "^2.0.2", "postcss": "^8.4.12", "prettier": "^2.4.1", "tailwindcss": "^3.0.24" @@ -11056,6 +11058,11 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -24449,6 +24456,11 @@ "universalify": "^2.0.0" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", diff --git a/package.json b/package.json index fe4e8182e70eaa9150b6c4bd6aa9e414408ee7a9..1a2ddb7600d61ce70a4bf73e48bccf3bfb399e50 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,14 @@ "axios": "^0.26.1", "core-js": "^3.8.3", "cssom": "^0.5.0", + "jwt-decode": "^3.1.2", "roboto-fontface": "*", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuelidate": "^0.7.7", "vuex": "^4.0.0", "vuex-persistedstate": "^4.1.0", - "webfontloader": "^1.0.0" + "webfontloader": "^1.6.28" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -41,6 +42,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "jest": "^27.0.5", + "jest-serializer-vue": "^2.0.2", "postcss": "^8.4.12", "prettier": "^2.4.1", "tailwindcss": "^3.0.24" diff --git a/src/App.vue b/src/App.vue index 5b76f23163ca815832df46238f55d797931d2c0c..98240aef8109abb6b658cd4d7c5cd21106cb354e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,17 +1,3 @@ <template> - <v-app> - <v-main> - <router-view /> - </v-main> - </v-app> + <router-view /> </template> - -<script> -export default { - name: "App", - - data: () => ({ - // - }), -}; -</script> diff --git a/src/assets/defaultUserProfileImage.jpg b/src/assets/defaultUserProfileImage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18c8cd70555d36af38f2bf164b0a22740b14cf6b Binary files /dev/null and b/src/assets/defaultUserProfileImage.jpg differ diff --git a/src/assets/removeIcon.png b/src/assets/removeIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..3c66b2a75ff7d81fd33622bf13a903f838f114ee Binary files /dev/null and b/src/assets/removeIcon.png differ diff --git a/src/components/CreateNewGroup.vue b/src/components/CreateNewGroup.vue new file mode 100644 index 0000000000000000000000000000000000000000..ceb5303249f9797e27e06fea8c189f9ab99a095f --- /dev/null +++ b/src/components/CreateNewGroup.vue @@ -0,0 +1,306 @@ +<template> + <div class="m-6"> + <!-- Component heading --> + <div class="flex justify-center mt-6"> + <p class="text-4xl">Opprett Gruppe</p> + </div> + + <!-- Radio boxes --> + <div class="mt-6"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="radioBoxLabel" + >Synlighet</label + > + <div class="form-check"> + <input + class="form-check-input appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" + type="radio" + name="flexRadioDefault" + id="flexRadioOpen" + value="Åpen" + @change="checkRadioButton($event)" + checked + /> + <label + class="form-check-label inline-block text-gray-800" + for="flexRadioOpen" + id="radioBoxOpenLabel" + > + Åpen + </label> + </div> + <div class="form-check"> + <input + class="form-check-input appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" + type="radio" + name="flexRadioDefault" + id="flexRadioPrivate" + value="Privat" + @change="checkRadioButton($event)" + /> + <label + class="form-check-label inline-block text-gray-800" + for="flexRadioPrivate" + id="radioBoxPrivateLabel" + > + Privat + </label> + </div> + </div> + + <!-- Title --> + <div class="mt-6" :class="{ error: v$.group.name.$errors.length }"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="titleLabel" + >Gruppenavn</label + > + <input + type="text" + id="title" + class="bg-gray-200 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + v-model="v$.group.name.$model" + required + /> + + <!-- error message for title--> + <div + class="text-red" + v-for="(error, index) of v$.group.name.$errors" + :key="index" + > + <div class="text-red-600 text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Select category --> + <div class="mt-6"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="selectCategoryLabel" + >Kategori</label + > + <select + v-model="v$.group.select.$model" + id="categories" + class="bg-gray-200 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + > + <option class="text-gray-400" value="" disabled selected> + Select a category + </option> + <option + v-for="category in group.categories" + :key="category" + class="text-gray-900 text-sm" + > + {{ category }} + </option> + </select> + + <!-- error message for select box --> + <div + class="text-red" + v-for="(error, index) of v$.group.select.$errors" + :key="index" + > + <div class="text-red-600 text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Description --> + <div class="mt-6" :class="{ error: v$.group.description.$errors.length }"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="descriptionLabel" + >Beskrivelse</label + > + <textarea + id="description" + rows="4" + v-model="v$.group.description.$model" + class="block p-2.5 w-full text-sm text-gray-900 bg-gray-200 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + required + ></textarea> + + <!-- error message for description --> + <div + class="text-red" + v-for="(error, index) of v$.group.description.$errors" + :key="index" + > + <div class="text-red-600 text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Images --> + <div class="mt-6"> + <label + class="block mb-2 text-xl font-medium text-gray-900 dark:text-gray-400" + id="imageLabel" + > + Bilde + </label> + + <input + type="file" + ref="file" + style="display: none" + @change="addImage" + multiple + accept="image/png, image/jpeg" + /> + + <!-- Button for adding an image --> + <div class="inline-flex rounded-md shadow-sm"> + <button + @click="$refs.file.click()" + class="text-black bg-gray-200 hover:bg-grey-800 focus:ring-4 focus:outline-none focus:ring-grey-300 font-medium rounded-lg text-sm sm:w-auto px-5 py-2.5 text-center dark:bg-grey-600 dark:hover:bg-grey-700 dark:focus:ring-grey-800 disabled:opacity-50 cursor-not-allowed" + :disabled="imageAdded" + > + Velg bilde + </button> + + <!-- Button for removing an image --> + <button + class="w-1/12 ml-5 text-white bg-white-500 font-medium rounded-lg text-sm" + v-show="imageAdded" + @click="removeImage" + > + <img src="../assets/removeIcon.png" alt="Remove icon image" /> + </button> + </div> + + <!-- Div box for showing all chosen images --> + <div v-for="image in group.images" :key="image" class="m-2"> + <img :src="image" class="w-1/2 inline" alt="Bilde av gjenstanden" /> + </div> + </div> + + <!-- Save item button --> + <div class="flex justify-center mt-10"> + <button + @click="saveClicked" + class="content-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" + id="saveButton" + > + Lagre + </button> + </div> + </div> +</template> + +<script> +import useVuelidate from "@vuelidate/core"; +import { required, helpers, maxLength } from "@vuelidate/validators"; + +export default { + name: "CreateNewGroup.vue", + + setup() { + return { v$: useVuelidate() }; + }, + + validations() { + return { + group: { + name: { + required: helpers.withMessage( + () => "Navnt kan ikke være tom", + required + ), + max: helpers.withMessage( + () => `Navnet kan være på max 50 tegn`, + maxLength(50) + ), + }, + description: { + required: helpers.withMessage( + () => "Beskrivelsen kan ikke være tom", + required + ), + max: helpers.withMessage( + () => `Beskrivelsen kan inneholde max 200 tegn`, + maxLength(200) + ), + }, + select: { + required: helpers.withMessage(() => `Velg en kategori`, required), + }, + }, + }; + }, + data() { + return { + group: { + name: "", + select: null, + description: "", + images: [], + categories: ["Borettslag", "Idrettsklubb", "Fritidsklubb"], + radio: null, + }, + imageThere: false, + }; + }, + computed: { + imageAdded: function () { + if (this.imageThere) { + return true; + } else { + return false; + } + }, + }, + methods: { + removeImage: function () { + this.group.images.pop(); + this.imageThere = false; + console.log("Bilder nå: " + this.group.images.length); + }, + checkRadioButton: function (event) { + this.group.radio = event.target.value; + console.log(this.group.radio); + }, + checkValidation: function () { + console.log("sjekker validering"); + + this.v$.group.$touch(); + if (this.v$.group.$invalid) { + console.log("Invalid, avslutter..."); + return false; + } + + console.log("validert!"); + return true; + }, + + async saveClicked() { + console.log("Attempting to save item"); + + if (this.checkValidation()) { + console.log("validert, videre..."); + console.log("Navn: " + this.group.name); + console.log("Synlighet: " + this.group.radio); + console.log("Kategori: " + this.group.select); + console.log("Beskrivelse: " + this.group.description); + console.log("bilder: " + this.group.images); + } + }, + + addImage: function (event) { + console.log(event.target.files); + this.group.images.push(URL.createObjectURL(event.target.files[0])); + console.log("antall bilder: " + this.group.images.length); + this.imageThere = true; + console.log("image: " + this.imageThere); + }, + }, +}; +</script> diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue index 84bd14e103584c8f13019ee33fe36e9a3f1e3ba3..ecc5bf651ccbb96fd5cc76ffaa9a204ea7d99310 100644 --- a/src/components/HelloWorld.vue +++ b/src/components/HelloWorld.vue @@ -1,78 +1,11 @@ <template> - <v-container> - <v-row class="text-center"> - <v-col cols="12"> - <v-img - :src="require('../assets/logo.svg')" - class="my-3" - contain - height="200" - /> - </v-col> + <div class="flex justify-center"> + <router-link to="/" class="m-6">Logg inn</router-link> - <v-col class="mb-4"> - <h1 class="display-2 font-weight-bold mb-3"> - Welcome to the Vuetify 3 Beta - </h1> + <router-link to="/register" class="m-6">Registrer deg</router-link> - <p class="subheading font-weight-regular"> - For help and collaboration with other Vuetify developers, - <br />please join our online - <a href="https://community.vuetifyjs.com" target="_blank" - >Discord Community</a - > - </p> - </v-col> - - <v-col class="mb-5" cols="12"> - <h2 class="headline font-weight-bold mb-5">What's next?</h2> - - <v-row justify="center"> - <a - v-for="(next, i) in whatsNext" - :key="i" - :href="next.href" - class="subheading mx-3" - target="_blank" - > - {{ next.text }} - </a> - </v-row> - </v-col> - - <v-col class="mb-5" cols="12"> - <h2 class="headline font-weight-bold mb-5">Important Links</h2> - - <v-row justify="center"> - <a - v-for="(link, i) in importantLinks" - :key="i" - :href="link.href" - class="subheading mx-3" - target="_blank" - > - {{ link.text }} - </a> - </v-row> - </v-col> - - <v-col class="mb-5" cols="12"> - <h2 class="headline font-weight-bold mb-5">Ecosystem</h2> - - <v-row justify="center"> - <a - v-for="(eco, i) in ecosystem" - :key="i" - :href="eco.href" - class="subheading mx-3" - target="_blank" - > - {{ eco.text }} - </a> - </v-row> - </v-col> - </v-row> - </v-container> + <router-link to="/about" class="m-6">Om BoCo</router-link> + </div> </template> <script> @@ -80,52 +13,7 @@ export default { name: "HelloWorld", data: () => ({ - ecosystem: [ - { - text: "vuetify-loader", - href: "https://github.com/vuetifyjs/vuetify-loader", - }, - { - text: "github", - href: "https://github.com/vuetifyjs/vuetify", - }, - { - text: "awesome-vuetify", - href: "https://github.com/vuetifyjs/awesome-vuetify", - }, - ], - importantLinks: [ - { - text: "Chat", - href: "https://community.vuetifyjs.com", - }, - { - text: "Made with Vuetify", - href: "https://madewithvuejs.com/vuetify", - }, - { - text: "Twitter", - href: "https://twitter.com/vuetifyjs", - }, - { - text: "Articles", - href: "https://medium.com/vuetify", - }, - ], - whatsNext: [ - { - text: "Explore components", - href: "https://vuetifyjs.com", - }, - { - text: "Roadmap", - href: "https://vuetifyjs.com/introduction/roadmap/", - }, - { - text: "Frequently Asked Questions", - href: "https://vuetifyjs.com/getting-started/frequently-asked-questions", - }, - ], - }), -}; + +}), +} </script> diff --git a/src/components/ItemCard.vue b/src/components/ItemCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..c7b866eefac807e242c8108a5b9323eb7159040d --- /dev/null +++ b/src/components/ItemCard.vue @@ -0,0 +1,31 @@ +<template> + <div class="mt-5"> + <div class="w-4/5 rounded bg-gray-200"> + <img + class="w-full" + :src="item.img || require('../assets/default-product.png')" + alt="Item image" + /> + <div class="p-1 m-1"> + <p class="text-gray-700 text-xs font-bold" id="adress"> + {{ item.adresse }} + </p> + <p class="font-bold text-sm" id="title">{{ item.title }}</p> + <p class="text-gray-700 text-xs" id="price">{{ item.price }} kr</p> + </div> + </div> + </div> +</template> + +<script> +export default { + props: { + item: { + img: String, + adresse: String, + title: String, + price: Number, + }, + }, +}; +</script> diff --git a/src/components/NewPasswordForm.vue b/src/components/NewPasswordForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..c86145ce10db183a6c8781b18c95c99c0e7c53b0 --- /dev/null +++ b/src/components/NewPasswordForm.vue @@ -0,0 +1,74 @@ +<template> + <div> + <v-col align="center" justify="space-around" class="mt-16"> + <v-img + max-width="45%" + :src="require('../assets/logo3.svg')" + align="center" + /> + </v-col> + + <v-form ref="form" v-model="valid" lazy-validation class="mt-8"> + <v-text-field + v-model="user.password" + :rules="[rules.required, rules.min]" + :type="'password'" + name="input-10-1" + label="Passord" + counter + ></v-text-field> + + <v-text-field + v-model="user.rePassword" + :rules="[rules.required, rules.min, rules.passwordConfirmation]" + :type="'password'" + name="input-10-1" + label="Confirm Password" + counter + ></v-text-field> + + <v-col justify="space-around" align="center"> + <v-btn + :disabled="!valid" + color="success" + class="mb-4 mt-4" + width="50%" + height="40px" + @click="setNewPassword" + > + Endre passord + </v-btn> + </v-col> + </v-form> + </div> +</template> + +<script> +export default { + name: "NewPasswordForm.vue", + + data() { + return { + user: { + password: "", + rePassword: "", + }, + + valid: true, + rules: { + required: (value) => !!value || "Feltet er påkrevd", + min: (v) => v.length >= 8 || "Minimum 8 tegn", + passwordConfirmation: (v) => + v === this.user.password || "Passordene må være like", + }, + }; + }, + + methods: { + async setNewPassword() {}, + validate() { + this.$refs.form.validate(); + }, + }, +}; +</script> diff --git a/src/components/RegisterFormComponent.vue b/src/components/RegisterFormComponent.vue index b69d38827a90245b00a8c961b4ef816511074bd6..240895bffbe2fe98be228f19e2d7549f3bc24227 100644 --- a/src/components/RegisterFormComponent.vue +++ b/src/components/RegisterFormComponent.vue @@ -1,155 +1,250 @@ <template> - <v-form ref="form" v-model="valid" lazy-validation> - <v-text-field - v-model="email" - :rules="emailRules" - label="E-mail" - required - ></v-text-field> - - <v-text-field - v-model="password" - :counter="32" - :rules="passwordRules" - label="Passord" - :append-icon="passwordHidden ? 'mdi-eye' : 'mdi-eye-off'" - :type="passwordHidden ? 'text' : 'password'" - @click:append="passwordHidden = !passwordHidden" - required - ></v-text-field> - - <v-container class="grey lighten-5"> - <v-row> - <v-text-field - class="pr-2" - v-model="firstName" - :counter="32" - :rules="firstNameRules" - label="Fornavn" - required - ></v-text-field> - - <v-text-field - class="pl-2" - v-model="lastName" - :counter="32" - :rules="lastNameRules" - label="Etternavn" - required - ></v-text-field> - </v-row> - </v-container> - - <v-text-field - v-model="address" - :counter="32" - :rules="addressRules" - label="Addresse" - required - ></v-text-field> - - <!-- <v-text-field - v-model="confirmPassword" - :rules="confirmPasswordRules" - label="Bekreft passord" - :append-icon="confirmPasswordHidden ? 'mdi-eye' : 'mdi-eye-off'" - :type="confirmPasswordHidden ? 'text' : 'password'" - @click:append="confirmPasswordHidden = !confirmPasswordHidden" - required - ></v-text-field> --> - - <!-- <v-select - v-model="select" - :items="items" - :rules="[(v) => !!v || 'Item is required']" - label="Item" - required - ></v-select> --> - - <!-- <v-checkbox - v-model="checkbox" - :rules="[(v) => !!v || 'You must agree to continue!']" - label="Do you agree?" - required - ></v-checkbox> --> - - <v-btn :disabled="!valid" color="success" class="mr-4" @click="submit()" - >Registrer</v-btn - > - - <v-btn color="error" class="mr-4" @click="reset()">Tøm felter</v-btn> - </v-form> + <section + class="max-w-4xl p-6 mx-auto bg-white rounded-md shadow-md dark:bg-gray-800" + > + <h2 class="text-lg font-semibold text-gray-700 capitalize dark:text-white"> + Opprett ny bruker + </h2> + + <form @submit.prevent> + <div class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2"> + <div> + <label class="text-gray-700 dark:text-gray-200" for="email" + >E-mail</label + > + <input + v-model="email" + id="email" + type="email" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="password" + >Passord</label + > + <input + v-model="password" + id="password" + type="password" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="confirmPassword" + >Bekreft Passord</label + > + <input + v-model="confirmPassword" + id="confirmPassword" + type="password" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="firstName" + >Fornavn</label + > + <input + data-test="firstNameTest" + v-model="firstName" + id="firstName" + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="lastName" + >Etternavn</label + > + <input + v-model="lastName" + id="lastName" + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="address" + >Addresse</label + > + <input + v-model="address" + id="address" + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + </div> + + <div class="flex justify-end mt-6"> + <button + class="px-6 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded-md hover:bg-gray-600 focus:outline-none focus:bg-gray-600" + @click="submit()" + type="submit" + :disabled="loading" + > + <div v-if="loading"> + <div v-if="loading" class="lds-ring"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> + <div v-else>Lagre</div> + </button> + </div> + </form> + </section> + <ul data-test="errorMessageList"> + <li v-if="errorMessage" data-test="customErrorMsg">{{ errorMessage }}</li> + <li v-for="error of v$.$errors" :key="error.$uid"> + <!-- {{ error.$validator }} --> + Field + {{ error.$property }} + has error: + {{ error.$message }} + </li> + </ul> </template> + <script> -import axios from "axios"; +import useVuelidate from "@vuelidate/core"; +import { doLogin, registerUser } from "@/utils/apiutil"; +import { required, email, minLength, sameAs } from "@vuelidate/validators"; -export default { - data: () => ({ - passwordHidden: false, - // confirmPasswordHidden: false, - valid: true, - firstName: "", - firstNameRules: [ - (v) => !!v || "Fornavn er påkrevd", - (v) => (v && v.length <= 32) || "Fornavn må være mindre enn 32 bokstaver", - ], - lastName: "", - lastNameRules: [ - (v) => !!v || "Etternavn er påkrevd", - (v) => - (v && v.length <= 32) || "Etternavn må være mindre enn 32 bokstaver", - ], - address: "", - addressRules: [ - (v) => !!v || "Addresse er påkrevd", - (v) => - (v && v.length <= 32) || "Addresse må være mindre enn 32 bokstaver", - ], - password: "", - passwordRules: [ - (v) => !!v || "Passord er påkrevd", - (v) => (v && v.length <= 32) || "Passord må være mindre enn 32 tegn", - (v) => (v && v.length >= 8) || "Passord må være større enn 8 tegn", - ], - // confirmPassword: "", - // confirmPasswordRules: [ - // (v) => !!v || "Passord er påkrevd", - // (v) => (v && v.length <= 32) || "Passord må være mindre enn 32 bokstaver", - // // (v) => v === this.password || "Passordene må være like", - // ], - email: "", - emailRules: [ - (v) => !!v || "E-mail is required", - (v) => /.+@.+\..+/.test(v) || "E-mail must be valid", - ], - // select: null, - // items: ["Item 1", "Item 2", "Item 3", "Item 4"], - // checkbox: false, - }), +// const isEmailTaken = (value) => +// fetch(`/api/unique/${value}`).then((r) => r.json()); // check the email in the server +export default { + setup: () => ({ v$: useVuelidate() }), + data() { + return { + errorMessage: "", + loading: false, + email: "", + password: "", + confirmPassword: "", + firstName: "", + lastName: "", + address: "", + }; + }, + validations() { + return { + email: { + required, + email, + // isUnique: helpers.withAsync(isEmailTaken), + }, + password: { + required, + minLength: minLength(8), + }, + confirmPassword: { sameAs: sameAs(this.password) }, + firstName: { required }, + lastName: { required }, + address: { required }, + }; + }, methods: { - submit() { - console.log("Attempting to register user"); - this.valid = this.$refs.form.validate(); - if (!this.valid) return; - this.valid = false; - console.log("User is validated"); - axios - .post("http://localhost:3000/api/register", { - email: this.email, - firstName: this.firstName, - lastname: this.lastName, - password: this.password, - address: this.address, - }) - .then(console.log("Sent")) - .catch((e) => console.log(e)); + async submit() { + //Display loading symbol + this.loading = true; + + //Validate form + const result = await this.v$.$validate(); + if (!result) { + this.loading = false; + return; + } + + //Send a request to create a user and save success as a bool + const userCreated = await this.sendRegisterRequest(); + + //If a user is created succsessfully, try to login + //If we get this far, we will be pushed anyway so there is no point updating "loading" + if (!userCreated) { + this.errorMessage = "Could not create user."; + return; + } + + const loginRequest = { + email: this.email, + password: this.password, + }; + + const loginResponse = await doLogin(loginRequest); + + if (loginResponse === "Failed login") { + this.errorMessage = "Failed to log in with new user"; + this.$store.commit("logout"); + this.$router.push("/login"); + return; + } + + this.$store.commit("saveToken", loginResponse); + this.$router.push("/"); }, - reset() { - this.$refs.form.reset(); - this.$refs.form.resetValidation(); - this.valid = true; + async sendRegisterRequest() { + const registerInfo = { + email: this.email, + firstName: this.firstName, + lastname: this.lastName, + password: this.password, + address: this.address, + }; + + const response = await registerUser(registerInfo); + + if (response.status === 200) return true; + return false; }, }, }; </script> + +<style scoped> +/* https://loading.io/css/ */ +.lds-ring { + display: inline-block; + position: relative; + width: 20px; + height: 20px; +} +.lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 16px; + height: 16px; + margin: 2px; + border: 2px solid #fff; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: #fff transparent transparent transparent; +} +.lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +</style> diff --git a/src/components/SearchItemListComponent.vue b/src/components/SearchItemListComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..69950032651b044300e126f2c828d41ae5d950d1 --- /dev/null +++ b/src/components/SearchItemListComponent.vue @@ -0,0 +1,82 @@ +<template> + <section class="relative w-full max-w-md px-5 py-4 mx-auto rounded-md"> + <div class="relative" id="searchComponent"> + <span class="absolute inset-y-0 left-0 flex items-center pl-3"> + <svg class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none"> + <path + d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + ></path> + </svg> + </span> + + <input + type="text" + id="searchInput" + class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none focus:ring" + placeholder="Search" + v-model="search" + /> + </div> + + <div class="absolute inset-x-0 px-6 py-3 mt-4 border-2 border-slate-500"> + <div class="grid grid-cols-2"> + <ItemCard v-for="item in searchedItems" :key="item" :item="item" /> + </div> + </div> + </section> +</template> + +<script> +import ItemCard from "@/components/ItemCard"; +export default { + name: "SearchItemListComponent", + + components: { + ItemCard, + }, + + computed: { + searchedItems() { + let filteredItems = []; + + filteredItems = this.items.filter( + (p) => + p.title.toLowerCase().includes(this.search.toLowerCase()) || + p.adresse.toLowerCase().includes(this.search.toLowerCase()) || + p.price === Number(this.search) + ); + + return filteredItems; + }, + }, + + /** + * Her må det lages en metode som henter alle items (i en gruppe) fra databasen. + * De kan deretter bli pusha inn i items array, og da burde de bli displayet i lista. + * Når denne metoden er på plass kan items[] i data tømmes. Da vil alt dataen komme fra db. + */ + + data() { + return { + items: [ + { img: "", adresse: "Oslo", title: "Dyson", price: 1000 }, + + { img: "", adresse: "Trondheim", title: "Gressklipper", price: 500 }, + + { img: "", adresse: "Bergen", title: "Bil", price: 500 }, + ], + item: { + img: "", + adresse: "", + title: "", + price: 0, + }, + search: "", + }; + }, +}; +</script> diff --git a/src/components/UserProfileComponents/LargeProfileCard.vue b/src/components/UserProfileComponents/LargeProfileCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..7c5c036f8103be9c42b62d0c818dfa229af2f8cd --- /dev/null +++ b/src/components/UserProfileComponents/LargeProfileCard.vue @@ -0,0 +1,141 @@ +<template> + <div + class="max-w-sm bg-white rounded-lg border border-gray-200 shadow-md dark:bg-gray-800 dark:border-gray-700" + > + <div v-show="isCurrentUser" class="flex justify-end px-4 pt-4"> + <button + id="dropdownDefault" + data-dropdown-toggle="dropdown" + @click="dropdown = !dropdown" + class="hidden sm:inline-block text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-1.5" + type="button" + > + <svg + class="w-6 h-6" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" + ></path> + </svg> + </button> + + <div + id="dropdown" + v-show="dropdown" + zindex="2" + class="z-10 w-44 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700" + > + <ul class="py-1" aria-labelledby="dropdownDefault"> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Mine gjenstander</router-link + > + </li> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Mine grupper + </router-link> + </li> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Leiehistorikk</router-link + > + </li> + <li> + <router-link + to="/newPassword" + class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Endre passord</router-link + > + </li> + <li> + <router-link + to="" + class="block py-2 px-4 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + >Slett bruker</router-link + > + </li> + </ul> + </div> + </div> + <div class="flex flex-col items-center pb-10"> + <img + class="mb-3 w-24 h-24 rounded-full shadow-lg" + src="../../assets/defaultUserProfileImage.jpg" + alt="Profile picture" + /> + <h5 class="mb-1 text-xl font-medium text-gray-900 dark:text-white"> + {{ user.firstName }} {{ user.lastName }} + </h5> + <div> + <rating-component :rating="renterRating" :ratingType="'Leietaker'" /> + <rating-component :rating="ownerRating" :ratingType="'Utleier'" /> + </div> + + <div v-show="!isCurrentUser" class="flex mt-4 space-x-3 lg:mt-6"> + <a + href="#" + class="inline-flex items-center py-2 px-4 text-sm font-medium text-center text-gray-900 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700" + >Åpne chat</a + > + </div> + </div> + </div> +</template> + +<script> +import RatingComponent from "@/components/UserProfileComponents/RatingComponent.vue"; +import { parseCurrentUser } from "@/utils/token-utils"; +import { getUser, getRenterRating, getOwnerRating } from "@/utils/apiutil"; +import router from "@/router"; + +export default { + name: "LargeProfileCard", + data() { + return { + user: {}, + currentUser: {}, + id: -1, + isCurrentUser: false, + renterRating: -1, //getRenterRating(this.userID), + ownerRating: -1, //getOwnerRating(this.userID), + dropdown: false, + }; + }, + components: { + RatingComponent, + }, + methods: { + async getUser() { + this.currentUser = parseCurrentUser(); + this.id = router.currentRoute.value.params.id; + if (this.id == this.currentUser.account_id) { + this.isCurrentUser = true; + this.user = this.currentUser; + return; + } + this.user = await getUser(this.id); + this.renterRating = getRenterRating(this.id); + this.ownerRating = getOwnerRating(this.id); + }, + getProfilePicture() { + /* if (this.user.picture != "") { + return this.user.picture; + } */ + return "../assets/defaultUserProfileImage.jpg"; + }, + }, + beforeMount() { + this.getUser(); + }, +}; +</script> diff --git a/src/components/UserProfileComponents/RatingComponent.vue b/src/components/UserProfileComponents/RatingComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..662b9607911697c89421e7c4727b44caa93dfe4c --- /dev/null +++ b/src/components/UserProfileComponents/RatingComponent.vue @@ -0,0 +1,61 @@ +<template> + <ul v-if="compRating != -1" class="flex justify-center"> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ ratingType }}: + </p> + </li> + <li v-for="i in 5" :key="i"> + <svg + :class="getFill(i)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + </li> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ compRating }} out of 5 + </p> + </li> + </ul> + <ul v-else class="flex justify-center"> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ ratingType }}: + </p> + </li> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + Rating ikke tilgjengelig + </p> + </li> + </ul> +</template> + +<script> +export default { + name: "RatingComponent", + data() { + return { + compRating: this.rating + 0, + }; + }, + props: { + rating: Number, + ratingType: String, + }, + methods: { + getFill(i) { + if (i <= this.rating) { + return "w-5 h-5 text-yellow-400"; + } + return "w-5 h-5 text-gray-300 dark:text-gray-500"; + }, + }, +}; +</script> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..4da140e21610587c8e88953ed9b17572c9bef98a --- /dev/null +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -0,0 +1,40 @@ +<template> + <div + class="select-none cursor-pointer hover:bg-gray-50 flex flex-1 items-center p-4" + > + <div class="flex flex-col w-10 h-10 justify-center items-center mr-4"> + <router-link to=""> + <img alt="profil" :src="getProfilePicture" /> + </router-link> + </div> + <div class="flex-1 pl-1"> + <div class="font-medium dark:text-white"> + {{ user.first_name }} {{ user.last_name }} + </div> + </div> + <div class="flex flex-row justify-center"> + <button class="w-10 text-right flex justify-end">Åpne chat</button> + <button v-if="admin" class="w-10 text-right flex justify-end"> + Fjern bruker + </button> + </div> + </div> +</template> + +<script> +export default { + name: "UserListItem", + props: { + user: Object, + admin: Boolean, + }, + methods: { + getProfilePicture() { + if (this.user.picture != "") { + return this.user.picture; + } + return "../assets/defaultUserProfileImage.jpg"; + }, + }, +}; +</script> diff --git a/src/main.js b/src/main.js index 34900f2ef3e7361998cbdee5516fd55fcd5c3ff7..32472cfc34b97f2df35e1d89704bc9d91e39c6d1 100644 --- a/src/main.js +++ b/src/main.js @@ -2,9 +2,6 @@ import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; -import { loadFonts } from "./plugins/webfontloader"; import "./index.css"; -loadFonts(); - createApp(App).use(router).use(store).mount("#app"); diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js deleted file mode 100644 index 8968900a4937011a5bcf7423449220a683056722..0000000000000000000000000000000000000000 --- a/src/plugins/vuetify.js +++ /dev/null @@ -1,9 +0,0 @@ -// Styles -import "@mdi/font/css/materialdesignicons.css"; -import "vuetify/styles"; - -// Vuetify -import { createVuetify } from "vuetify"; - -export default createVuetify(); -// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides diff --git a/src/plugins/webfontloader.js b/src/plugins/webfontloader.js deleted file mode 100644 index e86aa7db6b15c1b2bd2e3a40c7df2f2c7fa663bb..0000000000000000000000000000000000000000 --- a/src/plugins/webfontloader.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * plugins/webfontloader.js - * - * webfontloader documentation: https://github.com/typekit/webfontloader - */ - -export async function loadFonts() { - const webFontLoader = await import( - /* webpackChunkName: "webfontloader" */ "webfontloader" - ); - - webFontLoader.load({ - google: { - families: ["Roboto:100,300,400,500,700,900&display=swap"], - }, - }); -} diff --git a/src/utils/token-utils.js b/src/utils/token-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..ba37787fbb3f43cfdce7ba11aba70d2d3c8b5bff --- /dev/null +++ b/src/utils/token-utils.js @@ -0,0 +1,16 @@ +import jwt_decode from "jwt-decode"; +import store from "@/store"; + +export function tokenHeader() { + let token = store.state.user.token; + return { Authorization: token }; +} + +export function parseCurrentUser() { + let token = store.state.user.token; + return jwt_decode(token); +} + +export function parseUserFromToken(token) { + return jwt_decode(token); +} diff --git a/src/views/CreateNewGroupView.vue b/src/views/CreateNewGroupView.vue new file mode 100644 index 0000000000000000000000000000000000000000..d3b69d68220d7cd3a1cc3cd4fa4e3398feafe696 --- /dev/null +++ b/src/views/CreateNewGroupView.vue @@ -0,0 +1,15 @@ +<template> + <CreateNewGroup></CreateNewGroup> +</template> + +<script> +import CreateNewGroup from "@/components/CreateNewGroup"; +export default { + name: "CreateNewGroupView.vue", + components: { + CreateNewGroup, + }, +}; +</script> + +<style scoped></style> diff --git a/src/views/NewPasswordView.vue b/src/views/NewPasswordView.vue new file mode 100644 index 0000000000000000000000000000000000000000..bcccd80e96de1cf888b6cc0adb82baa0e48fc8fd --- /dev/null +++ b/src/views/NewPasswordView.vue @@ -0,0 +1,23 @@ +<template> + <div class="newPasswordPage"> + <NewPasswordForm></NewPasswordForm> + </div> +</template> + +<script> +import NewPasswordForm from "@/components/NewPasswordForm"; +export default { + name: "NewPasswordView.vue", + components: { + NewPasswordForm, + }, +}; +</script> + +<style scoped> +.newPasswordPage { + background-color: white; + height: 100%; + overflow: auto; +} +</style> diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue new file mode 100644 index 0000000000000000000000000000000000000000..cfd2cefd902017ad7b867b7cfe447cce5648cf07 --- /dev/null +++ b/src/views/ProfileView.vue @@ -0,0 +1,14 @@ +<!-- View for looking at different profile display methods --> +<template> + <large-profile-card :isCurrentUser="true" /> +</template> + +<script> +import LargeProfileCard from "@/components/UserProfileComponents/LargeProfileCard.vue"; + +export default { + components: { + LargeProfileCard, + }, +}; +</script> diff --git a/src/views/RegisterView.vue b/src/views/RegisterView.vue index 9d2ee691f97a923ea3fde5a7d1881792a252b801..c1478be27a6919308ca75adad5a1df3aca8303a3 100644 --- a/src/views/RegisterView.vue +++ b/src/views/RegisterView.vue @@ -1,5 +1,7 @@ <template> - <register-form-component id="form" class="pa-8" /> + <div class="h-screen bg-gray-200 content-center grid place-items-center"> + <RegisterFormComponent /> + </div> </template> <script> @@ -11,10 +13,3 @@ export default { }, }; </script> - -<style scoped> -#form { - max-width: 600px; - margin: auto; -} -</style> diff --git a/src/views/SearchItemListView.vue b/src/views/SearchItemListView.vue new file mode 100644 index 0000000000000000000000000000000000000000..288fa367f2da5f0df7f164f4538a35ecfca62db1 --- /dev/null +++ b/src/views/SearchItemListView.vue @@ -0,0 +1,15 @@ +<template> + <SearchItemListComponent></SearchItemListComponent> +</template> + +<script> +import SearchItemListComponent from "@/components/SearchItemListComponent"; +export default { + name: "SearchItemListView", + components: { + SearchItemListComponent, + }, +}; +</script> + +<style scoped></style> diff --git a/tailwind.config.js b/tailwind.config.js index f521631db48dc78b5a6597a6552d9d95ecaee4a5..7cd4f37efd95627b8f362e1b210fd12b384925d9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,5 @@ module.exports = { + darkMode: "class", content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], theme: { extend: {}, diff --git a/tests/unit/RegisterUserComponent.spec.js b/tests/unit/RegisterUserComponent.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a13d18e437230618a1d38e19b9c7bb00d27c1d7f --- /dev/null +++ b/tests/unit/RegisterUserComponent.spec.js @@ -0,0 +1,50 @@ +import { mount } from "@vue/test-utils"; +import RegisterFormComponent from "@/components/RegisterFormComponent"; + +describe("RegisterFormComponent", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(RegisterFormComponent); + }); + + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it("is instantiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("renders error message to user", async () => { + await wrapper.setData({ errorMessage: "test message" }); + expect(wrapper.find('li[data-test="customErrorMsg"]').text()).toBe( + "test message" + ); + }); + + it("renders the h2 text correctly", () => { + expect(wrapper.find("h2").text()).toBe("Opprett ny bruker"); + }); + + it("has a button", () => { + expect(wrapper.exists("button")).toBe(true); + }); + + it("updates data when field is updated", async () => { + await wrapper.find('input[data-test="firstNameTest"]').setValue("Gunnar"); + expect(wrapper.vm.firstName).toBe("Gunnar"); + }); + + it("displays 5 error messages when submit is clicked with no data", async () => { + await wrapper.find("button").trigger("click"); + expect(wrapper.findAll("li").length).toBe(5); + }); + + /* it("button click with correct sum", () => { + wrapper.setData({ guess: "15" }); + const button = wrapper.find("button"); + button.trigger("click"); + expect(wrapper.vm.message).toBe("SUCCESS!"); + }); */ +}); diff --git a/tests/unit/__snapshots__/RegisterUserComponent.spec.js.snap b/tests/unit/__snapshots__/RegisterUserComponent.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..f1a63d05fc1ff431b8677b83e799442934955b68 --- /dev/null +++ b/tests/unit/__snapshots__/RegisterUserComponent.spec.js.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RegisterFormComponent renders correctly 1`] = ` +<div + data-v-app="" +> + + <section + class="max-w-4xl p-6 mx-auto bg-white rounded-md shadow-md dark:bg-gray-800" + > + <h2 + class="text-lg font-semibold text-gray-700 capitalize dark:text-white" + > + Opprett ny bruker + </h2> + <form> + <div + class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2" + > + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="email" + > + E-mail + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="email" + type="email" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="password" + > + Passord + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="password" + type="password" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="confirmPassword" + > + Bekreft Passord + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="confirmPassword" + type="password" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="firstName" + > + Fornavn + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + data-test="firstNameTest" + id="firstName" + type="text" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="lastName" + > + Etternavn + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="lastName" + type="text" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="address" + > + Addresse + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="address" + type="text" + /> + </div> + </div> + <div + class="flex justify-end mt-6" + > + <button + class="px-6 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded-md hover:bg-gray-600 focus:outline-none focus:bg-gray-600" + type="submit" + > + <div> + Lagre + </div> + </button> + </div> + </form> + </section> + <ul + data-test="errorMessageList" + > + <!--v-if--> + + + </ul> + +</div> +`; diff --git a/tests/unit/apiutil-login-mock.spec.js b/tests/unit/apiutil-login-mock.spec.js index b1c99b4c45466356b8b75bb354c1c52253edc156..b6d573beb45aff0450d4d52334a8e69cce950128 100644 --- a/tests/unit/apiutil-login-mock.spec.js +++ b/tests/unit/apiutil-login-mock.spec.js @@ -4,42 +4,39 @@ import axios from "axios"; jest.mock("axios"); describe("testing mocking of apiutil.js", () => { + it("check that login fails with wrong credentials - against mock", async () => { - // mock api response on POST call (once) - const expectedLoginResponse = { response: "Login failed" }; + + const loginRequest = { + email: "wrong@email.com", + password: "thisiswrong123"}; + + const expectedLoginResponse = { isLoggedIn: false, token: "" } + axios.post.mockImplementation(() => Promise.resolve({ data: expectedLoginResponse }) ); - // do the call - const loginRequest = { - email: "wrong@email.com", - password: "thisiswrong123", - }; const loginResponse = await doLogin(loginRequest); - // check response - // note that even if wrong username and password are used, mock is configured to return Success - expect(loginResponse).toEqual(expectedLoginResponse); + expect(loginResponse.token.isLoggedIn).toEqual(expectedLoginResponse.isLoggedIn); }); + it("check that login succeeds when correct credentials - against mock", async () => { - // mock api response on POST call (once) - const apiResponse = { - response: - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", - }; - const expectedLoginResponse = { response: "Login failed" }; - axios.post.mockImplementation(() => Promise.resolve({ data: apiResponse })); - // do the call const loginRequest = { email: "correct@email.com", - password: "thisiscorrect123", - }; + password: "thisiscorrect123"}; + + const apiResponse = {isLoggedIn: true, token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM" + + "0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}; + + const expectedLoginResponse = {isLoggedIn: false, token: ""}; + + axios.post.mockImplementation(() => Promise.resolve({ data: apiResponse })); + const loginResponse = await doLogin(loginRequest); - // check response - // note that even if wrong username and password are used, mock is configured to return Success - expect(loginResponse).not.toEqual(expectedLoginResponse); + expect(loginResponse.token.isLoggedIn).not.toEqual(expectedLoginResponse.isLoggedIn); }); }); diff --git a/tests/unit/apiutil-user-mock.spec.js b/tests/unit/apiutil-user-mock.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..73612f0f3579f5c04b99d2424a1a57fd62ff31bc --- /dev/null +++ b/tests/unit/apiutil-user-mock.spec.js @@ -0,0 +1,28 @@ +import { getUser } from "@/utils/apiutil"; +import axios from "axios"; + +jest.mock("axios"); + +describe("testing mocking of apiutil.js", () => { + it("check that existing user returns correctly", async () => { + const expectedResponse = { + response: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2NvdW50X2lkIjoiNiIsImV4cCI6MTY1MTEzMDU2NywiZmlyc3RfbmFtZSI6IkFsaWRhIiwiZW1haWwiOiJhbGlkYUB0ZXN0Lm5vIn0.Cp3_qfLhA55j5yaa1WPG97LNtvAZssxo0ROP3VIrHVs", + }; + axios.get.mockImplementation(() => + Promise.resolve({ data: expectedResponse }) + ); + + const userResponse = await getUser(1); + expect(userResponse).not.toEqual({ response: "User not found in DB" }); + }); + it("check that non-existing user returns 404", async () => { + const expectedResponse = { response: "User not found in DB" }; + axios.get.mockImplementation(() => + Promise.resolve({ data: expectedResponse }) + ); + + const userResponse = await getUser(100000); + expect(userResponse).toEqual(expectedResponse); + }); +}); diff --git a/tests/unit/create-new-group.spec.js b/tests/unit/create-new-group.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..2b269b082ece04c8d1cbfa9c8eb9233d0d4e00f8 --- /dev/null +++ b/tests/unit/create-new-group.spec.js @@ -0,0 +1,49 @@ +import { shallowMount } from "@vue/test-utils"; +import CreateNewGroup from "@/components/CreateNewGroup.vue"; + +describe("CreateNewGroup elements rendering", () => { + + it("renders all labels", () => { + + const wrapper = shallowMount(CreateNewGroup); + + expect(wrapper.find('#radioBoxLabel').text()).toMatch("Synlighet"); + expect(wrapper.find('#radioBoxOpenLabel').text()).toMatch("Åpen"); + expect(wrapper.find('#radioBoxPrivateLabel').text()).toMatch("Privat"); + expect(wrapper.find('#titleLabel').text()).toMatch("Gruppenavn"); + expect(wrapper.find('#selectCategoryLabel').text()).toMatch("Kategori"); + expect(wrapper.find('#descriptionLabel').text()).toMatch("Beskrivelse"); + expect(wrapper.find('#imageLabel').text()).toMatch("Bilde"); + + }); + + it("Tests setting values of input field", async() => { + + const wrapper = shallowMount(CreateNewGroup); + + const titleInput = wrapper.find('#title'); + await titleInput.setValue("Fjellgata"); + expect(titleInput.element.value).toBe("Fjellgata"); + + const selectedCategory = wrapper.find('#categories'); + await selectedCategory.setValue("Borettslag"); + expect(selectedCategory.element.value).toBe("Borettslag"); + + const descriptionInput = wrapper.find('#description'); + await descriptionInput.setValue("Dette er et borettslag"); + expect(descriptionInput.element.value).toBe("Dette er et borettslag"); + }); + + it("Tests if radio box checks", async() => { + + const wrapper = shallowMount(CreateNewGroup); + + const radioInputOpen = wrapper.find('#flexRadioOpen'); + await radioInputOpen.setChecked(); + expect(radioInputOpen.element.checked).toBeTruthy(); + + const radioInputPrivate = wrapper.find('#flexRadioPrivate'); + await radioInputPrivate.setChecked(); + expect(radioInputPrivate.element.checked).toBeTruthy(); + }); +}); diff --git a/tests/unit/search-item-list.spec.js b/tests/unit/search-item-list.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6a01bbbdbe83b35b070e3fc9bf8ac9ed061ab6af --- /dev/null +++ b/tests/unit/search-item-list.spec.js @@ -0,0 +1,15 @@ +import { shallowMount } from "@vue/test-utils"; +import SearchItemListComponent from "@/components/SearchItemListComponent.vue"; + +describe("CreateNewGroup elements rendering", () => { + + it("Tests setting values of input field", async() => { + + const wrapper = shallowMount(SearchItemListComponent); + + const searchInput = wrapper.find('#searchInput'); + await searchInput.setValue("Dyson"); + expect(searchInput.element.value).toBe("Dyson"); + + }); +});