diff --git a/src/App.vue b/src/App.vue index 1a140ee58a2d942ddacb457b5fbd6f5b5ad23e0b..12ad03a6b286a47ed6a392e7338562c82b213f64 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,5 +14,7 @@ import ErrorBoundaryCatcher from '@/components/Exceptions/ErrorBoundaryCatcher.v <style> main { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-weight: 600; + } </style> \ No newline at end of file diff --git a/src/assets/Sparesti-logo.png b/src/assets/Sparesti-logo.png index 22c480c6044969c4c3352cd33f20a3a0334441b9..edbd70f919853607975310bd02e62da2bbc7d53d 100644 Binary files a/src/assets/Sparesti-logo.png and b/src/assets/Sparesti-logo.png differ diff --git a/src/assets/icons/feedback.svg b/src/assets/icons/feedback.svg new file mode 100644 index 0000000000000000000000000000000000000000..796e4346c08bed42140a3d053a93e4a5dee90093 --- /dev/null +++ b/src/assets/icons/feedback.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M240-399h313v-60H240v60Zm0-130h480v-60H240v60Zm0-130h480v-60H240v60ZM80-80v-740q0-24 18-42t42-18h680q24 0 42 18t18 42v520q0 24-18 42t-42 18H240L80-80Zm134-220h606v-520H140v600l74-80Zm-74 0v-520 520Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/leaderboard.svg b/src/assets/icons/leaderboard.svg new file mode 100644 index 0000000000000000000000000000000000000000..a5859255eb4c0914af29429319172e7d19eef324 --- /dev/null +++ b/src/assets/icons/leaderboard.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M140-180h187v-360H140v360Zm247 0h186v-600H387v600Zm246 0h187v-280H633v280ZM80-120v-480h247v-240h306v320h247v400H80Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/logout.svg b/src/assets/icons/logout.svg new file mode 100644 index 0000000000000000000000000000000000000000..1c5e27e1a1886b2c5acc18c7aca5de957f4569f2 --- /dev/null +++ b/src/assets/icons/logout.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h299v60H180v600h299v60H180Zm486-185-43-43 102-102H360v-60h363L621-612l43-43 176 176-174 174Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/newsletter.svg b/src/assets/icons/newsletter.svg new file mode 100644 index 0000000000000000000000000000000000000000..e9b493b41c32538c4a52fc2224d8670aa4288e5d --- /dev/null +++ b/src/assets/icons/newsletter.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M140-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h680q24 0 42 18t18 42v600q0 24-18 42t-42 18H140Zm0-60h680v-600H140v600Zm109-106h462v-60H249v60Zm0-166h155v-222H249v222Zm259 0h203v-60H508v60Zm0-162h203v-60H508v60ZM140-180v-600 600Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/person.svg b/src/assets/icons/person.svg new file mode 100644 index 0000000000000000000000000000000000000000..ef3c9d45ac4c8345e1444ae4361c56641ad4b0c0 --- /dev/null +++ b/src/assets/icons/person.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M480-481q-66 0-108-42t-42-108q0-66 42-108t108-42q66 0 108 42t42 108q0 66-42 108t-108 42ZM160-160v-94q0-38 19-65t49-41q67-30 128.5-45T480-420q62 0 123 15.5t127.921 44.694q31.301 14.126 50.19 40.966Q800-292 800-254v94H160Zm60-60h520v-34q0-16-9.5-30.5T707-306q-64-31-117-42.5T480-360q-57 0-111 11.5T252-306q-14 7-23 21.5t-9 30.5v34Zm260-321q39 0 64.5-25.5T570-631q0-39-25.5-64.5T480-721q-39 0-64.5 25.5T390-631q0 39 25.5 64.5T480-541Zm0-90Zm0 411Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/saving.svg b/src/assets/icons/saving.svg new file mode 100644 index 0000000000000000000000000000000000000000..d90502d4010c6e9c6a379c96621e3db8dfdae386 --- /dev/null +++ b/src/assets/icons/saving.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M640-520q17 0 28.5-11.5T680-560q0-17-11.5-28.5T640-600q-17 0-28.5 11.5T600-560q0 17 11.5 28.5T640-520ZM320-620h200v-60H320v60ZM180-120q-34-114-67-227.5T80-580q0-92 64-156t156-64h200q29-38 70.5-59t89.5-21q25 0 42.5 17.5T720-820q0 6-1.5 12t-3.5 11q-4 11-7.5 22.5T702-751l91 91h87v279l-113 37-67 224H480v-80h-80v80H180Zm45-60h115v-80h200v80h115l63-210 102-35v-175h-52L640-728q1-25 6.5-48.5T658-824q-38 10-72 29.5T534-740H300q-66.286 0-113.143 46.857T140-580q0 103.158 29 201.579T225-180Zm255-322Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/settings.svg b/src/assets/icons/settings.svg new file mode 100644 index 0000000000000000000000000000000000000000..542fa092683df8d0777dd62eedfe9260983b14e1 --- /dev/null +++ b/src/assets/icons/settings.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m388-80-20-126q-19-7-40-19t-37-25l-118 54-93-164 108-79q-2-9-2.5-20.5T185-480q0-9 .5-20.5T188-521L80-600l93-164 118 54q16-13 37-25t40-18l20-127h184l20 126q19 7 40.5 18.5T669-710l118-54 93 164-108 77q2 10 2.5 21.5t.5 21.5q0 10-.5 21t-2.5 21l108 78-93 164-118-54q-16 13-36.5 25.5T592-206L572-80H388Zm48-60h88l14-112q33-8 62.5-25t53.5-41l106 46 40-72-94-69q4-17 6.5-33.5T715-480q0-17-2-33.5t-7-33.5l94-69-40-72-106 46q-23-26-52-43.5T538-708l-14-112h-88l-14 112q-34 7-63.5 24T306-642l-106-46-40 72 94 69q-4 17-6.5 33.5T245-480q0 17 2.5 33.5T254-413l-94 69 40 72 106-46q24 24 53.5 41t62.5 25l14 112Zm44-210q54 0 92-38t38-92q0-54-38-92t-92-38q-54 0-92 38t-38 92q0 54 38 92t92 38Zm0-130Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/assets/icons/storefront.svg b/src/assets/icons/storefront.svg new file mode 100644 index 0000000000000000000000000000000000000000..e259d836feaf2d2600d3311bcd713d8b0555a9bc --- /dev/null +++ b/src/assets/icons/storefront.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M840-519v339q0 24-18 42t-42 18H179q-24 0-42-18t-18-42v-339q-28-24-37-59t2-70l43-135q8-27 28-42t46-15h553q28 0 49 15.5t29 41.5l44 135q11 35 1.5 70T840-519Zm-270-31q29 0 49-19t16-46l-25-165H510v165q0 26 17 45.5t43 19.5Zm-187 0q28 0 47.5-19t19.5-46v-165H350l-25 165q-4 26 14 45.5t44 19.5Zm-182 0q24 0 41.5-16.5T263-607l26-173H189l-46 146q-10 31 8 57.5t50 26.5Zm557 0q32 0 50.5-26t8.5-58l-46-146H671l26 173q3 24 20.5 40.5T758-550ZM179-180h601v-311q1 1-6.5 1H758q-25 0-47.5-10.5T666-533q-16 20-40 31.5T573-490q-30 0-51.5-8.5T480-527q-15 18-38 27.5t-52 9.5q-31 0-55-11t-41-32q-24 21-47 32t-46 11h-13.5q-6.5 0-8.5-1v311Zm601 0H179h601Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/components/BaseComponents/Footer.vue b/src/components/BaseComponents/Footer.vue index 4df3ae59a8d6290c679910dcd5cfb4ad0d273c18..7fddbaae0fe94207787bd1d31cc1c2798eeabaa9 100644 --- a/src/components/BaseComponents/Footer.vue +++ b/src/components/BaseComponents/Footer.vue @@ -1,9 +1,11 @@ <template> <div> - <footer class="text-center text-white bg-danger-subtle" style="width: 100%"> + <footer id = "footer" class="text-center text-white" style="width: 100%"> <div class="text-center p-3"> © 2024 Copyright: Anders Høvik, Andreas Svendsrud, Henrik Dybdal, Henrik Sandok, Jens Aanestad, Victor Kaste, Viktor Grevskott </div> </footer> </div> </template> + +<style scoped> #footer {background-color: #0A58CA;}</style> diff --git a/src/components/BaseComponents/Menu.vue b/src/components/BaseComponents/Menu.vue index 35f6e8f10a7df8efbb4aec5e020496130fd3f1cf..038fc7c0f2871f0b2bdaa3de736c9a54a1d2009e 100644 --- a/src/components/BaseComponents/Menu.vue +++ b/src/components/BaseComponents/Menu.vue @@ -1,8 +1,9 @@ <template> - <nav class="navbar navbar-expand-lg bg-success"> - <div class="container-fluid" > + <nav id="navBar" class="navbar navbar-expand-xl"> + <div class="container-fluid"> <a class="navbar-brand" href="/" @click="toHome"> - <img src="/src/assets/Sparesti-logo.png" alt="Sparesti-logo" width="60"> + <img id="logoImg" src="/src/assets/Sparesti-logo.png" alt="Sparesti-logo" width="60"> + <span id="logo" class="text-white">Sparesti</span> </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" @@ -12,74 +13,76 @@ <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav ms-auto mb-2 mb-lg-0 ui-menu"> <li class="nav-item"> - <a class="nav-link text-white" href="/news" @click="toSavingGoals">Saving goals</a> + <a class="nav-link text-white" href="#" @click="toSavingGoals"><img + src="@/assets/icons/saving.svg">Saving goals</a> </li> <li class="nav-item"> - <a class="nav-link text-white" href="/news" @click="toLeaderboard">Leaderboard</a> + <a class="nav-link text-white" href="#" @click="toLeaderboard"><img + src="@/assets/icons/leaderboard.svg">Leaderboard</a> + </li> + <li class="nav-item"> + <a class="nav-link text-white" href="#" @click="toNews"><img + src="@/assets/icons/newsletter.svg">News</a> + </li> + <li class="nav-item"> + <a class="nav-link text-white" href="#" @click="toStore"><img + src="@/assets/icons/storefront.svg">Store</a> + </li> + <li class="nav-item dropdown"> + <a class="nav-link dropdown-toggle username-text text-white " href="#" role="button" + data-bs-toggle="dropdown" aria-expanded="false"> + <img src="@/assets/icons/person.svg">Username + </a> + <ul class="dropdown-menu dropdown-username-content"> + <li><a class="dropdown-item text-white dropdown-username-link" href="#" + @click="toUserProfile"><img src="@/assets/icons/person.svg">User Profile</a></li> + <li><a class="dropdown-item text-white dropdown-username-link" href="#" + @click="toSetting"><img src="@/assets/icons/settings.svg">Setting</a></li> + <li><a class="dropdown-item text-white dropdown-username-link" href="#" + @click="toFeedback"><img src="@/assets/icons/feedback.svg">Feedback</a></li> + <li><a class="dropdown-item text-white dropdown-username-link" href="#" + @click="toFeedback"><img src="@/assets/icons/logout.svg">Log out</a></li> + </ul> </li> - <li class="nav-item"> - <a class="nav-link text-white" href="/news" @click="toNews">News</a> - </li> - <li class="nav-item"> - <a class="nav-link text-white" href="/news" @click="toStore">Store</a> - </li> - </ul> - <nav class="navbar bg-success"> - <div class="container-fluid"> - <a class="nav-link dropdown-toggle username-text text-white " href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> - <img src="/src/assets/userprofile.png" alt="Userprofile" width="30" height="30" class="d-inline-block align-text-top"> - Username - </a> - <ul class="dropdown-menu bg-success dropdown-username-content"> - <li><router-link class="dropdown-item text-white dropdown-username-link" to="/profile">User Profile</router-link></li> - <li><a class="dropdown-item text-white dropdown-username-link" href="/news" @click="toSetting">Setting</a></li> - <li><a class="dropdown-item text-white dropdown-username-link" href="/news" @click="toFeedback">Feedback</a></li> </ul> - </div> - </nav> </div> </div> </nav> </template> <script setup lang="ts"> - - -/** - * May need to change from a-links to routerlinks to avoid complications with href. - */ - -import {useRouter} from "vue-router"; +import { useRouter } from "vue-router"; const router = useRouter(); -function toHome(){ - router.push('/') +function toHome() { + router.push('/') } -function toSavingGoals(){ - router.push('/news') +function toSavingGoals() { + router.push('/news') } -function toLeaderboard(){ - router.push('/news') +function toLeaderboard() { + router.push('/news') } -function toNews(){ - router.push('/news') +function toNews() { + router.push('/news') } -function toStore(){ - router.push('/news') +function toStore() { + router.push('/news') } -function toSetting(){ - router.push('/news') +function toSetting() { + router.push('/news') } -function toFeedback(){ - router.push('/news') +function toFeedback() { + router.push('/news') } + function toUserProfile(){ router.push('/profile') } @@ -87,20 +90,81 @@ function toUserProfile(){ </script> <style scoped> -.ui-menu{ - font-size: 150%; +.navbar-brand { + display: flex; + align-items: center; +} + +.navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='rgba(255, 255, 255)' stroke-width='2' stroke-linecap='round' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); +} + +.nav-item { + padding: 0.3rem 0.6rem; + font-size: 1.7rem; +} + +.nav-item:hover { + background-color: #2b6ac7; } -.username-text{ - font-size: 150%; +.nav-item .dropdown { + display: flex; + justify-content: center; +} + +.nav-link { + display: flex; + align-items: center; + justify-content: center; +} + +.dropdown-item { + width: 100%; + display: flex; + justify-content: center; +} + +.dropdown-menu { + background-color: #0A58CA; +} + +.dropdown-username-link { + font-size: 1.7rem; + display: flex; + justify-self: center; +} + +.dropdown-username-link:hover { + background-color: #2b6ac7; +} + +#navBar { + background-color: #0A58CA; +} + +.navbar { + display: flex; + align-items: center; +} + +.container-fluid { + font-size: 1.7rem; +} +#logo { + font-size: 2.5rem; + height: 100%; } -.dropdown-username-content{ - font-size: 150%; +.nav-link img { + margin-right: 5px; } -.dropdown-username-link:hover{ - background-color: #538d53 +#logoImg { + margin-right: 0.3rem; + width: 75px; + height: auto; + aspect-ratio: 1.3/1; } </style> \ No newline at end of file diff --git a/src/components/InputFields/BaseInput.vue b/src/components/InputFields/BaseInput.vue index 5558323b9a95e53825a222ac444c8a5ee5f8f23a..516faeb54655832d42eab147d6e3b38a16f89f1f 100644 --- a/src/components/InputFields/BaseInput.vue +++ b/src/components/InputFields/BaseInput.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> +const emit = defineEmits(['inputChangeEvent']); const props = defineProps({ label: { type: String, @@ -16,19 +17,33 @@ const props = defineProps({ inputId: { type: String, required: true + }, + modelValue: { + type: String, + default: "" + }, + isValid: { + type: Boolean, + default: false } }); + +const onInputEvent = (event: any) => { + emit('inputChangeEvent', event.target.value) +} </script> <template> <div> <label :for="inputId">{{ label }}</label> - <input :type="props.type" + <input :value="props.modelValue" + @input="onInputEvent" + :type="props.type" class="form-control" :placeholder="props.placeholder" :id="inputId" required /> - <div class="invalid-feedback">Invalid {{ label }}</div> - <div class="valid-feedback">Correct {{ label }}</div> + <div v-if="props.isValid" class="invalid-feedback">Invalid {{ label }}</div> + <div v-else class="valid-feedback">Valid {{ label }}</div> </div> </template> diff --git a/src/components/LeaderboardComponents/Leaderboard.vue b/src/components/LeaderboardComponents/Leaderboard.vue new file mode 100644 index 0000000000000000000000000000000000000000..48317805da23489922b3e7bd0cffbbca7fe30da7 --- /dev/null +++ b/src/components/LeaderboardComponents/Leaderboard.vue @@ -0,0 +1,161 @@ +<template> + <div id="leaderboard"> + <div class="ribbon"></div> + <table> + <tr v-for="(entry, index) in leaderboard" :key="entry.user.id"> + <td class="number">{{ index + 1 }}</td> + <td class="name" @click="navigateToUserProfile(entry.user.id)">{{ entry.user.username }}</td> + <td class="points" v-if="index === 0"> + {{ entry.score }} + <div class = "medal"> + <img class="gold-medal" src="https://github.com/malunaridev/Challenges-iCodeThis/blob/master/4-leaderboard/assets/gold-medal.png?raw=true" alt="gold medal" /> + </div> + </td> + <td v-else class="points">{{ entry.score }}</td> + </tr> + </table> + </div> + </template> + + <script setup lang="ts"> + import { ref } from 'vue'; + import { useRouter } from 'vue-router'; + + const router = useRouter(); + + const props = defineProps({ + leaderboard: { + type: Array, + required: true + } + }); + + const navigateToUserProfile = () => { + router.push({ name: 'news' }); + }; + </script> + + <style scoped> + #leaderboard { + width: 100%; + position: relative; + } + + table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + color: #141a39; + cursor: default; + } + + tr { + transition: all 0.2s ease-in-out; + border-radius: 0.2rem; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + } + + tr:not(:first-child):hover { + background-color: #fff; + transform: scale(1.1); + -webkit-box-shadow: 0px 5px 15px 8px #e4e7fb; + box-shadow: 0px 5px 15px 8px #e4e7fb; + } + + tr:nth-child(even) { + background-color: #f9f9f9; + } + + tr:nth-child(1) { + color: #fff; + } + + td { + height: 2rem; + font-family: "Rubik", sans-serif; + font-size: 1.4rem; + padding: 1rem 2rem; + position: relative; + } + + .number { + width: 1rem; + font-size: 2.2rem; + font-weight: bold; + text-align: left; + display: flex; + align-items: center; + } + + .name { + font-size: 1.3rem; + cursor: pointer; + display: flex; + align-items: center; + } + + .points { + font-weight: bold; + font-size: 1.3rem; + display: flex; + justify-content: flex-end; + align-items: center; + } + + @media (max-width: 1000px) { + .number .name .points { + font-size: 0.5rem; + } + + td { + padding: 0.2rem 0.5rem; + } + } + + + .points:first-child { + width: 10rem; + } + + .gold-medal { + height: 3rem; + margin-left: 1.5rem; + } + + .ribbon { + width: 100%; + height: 4.5rem; + top: -0.5rem; + background-color: #0A58CA; + position: absolute; + left: -1rem; + box-shadow: 0px 15px 11px -6px #7a7a7d; + } + + .ribbon::before { + content: ""; + height: 1.5rem; + width: 1.5rem; + bottom: -0.8rem; + left: 0.35rem; + transform: rotate(45deg); + background-color: #0A58CA; + position: absolute; + z-index: -1; + } + + .ribbon::after { + content: ""; + height: 1.5rem; + width: 1.5rem; + bottom: -0.8rem; + right: 0.35rem; + transform: rotate(45deg); + background-color: #0A58CA; + position: absolute; + z-index: -1; + } + </style> \ No newline at end of file diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 6b0da2a71cf6b6b87f27c8feaff2ef259c951520..150711c35baec1405f70edda8b6584bf759682e2 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -1,50 +1,62 @@ <script setup lang="ts"> import BaseInput from '@/components/InputFields/BaseInput.vue' import Button1 from '@/components/Buttons/Button1.vue' +import { ref } from 'vue' + +const emailRef = ref() +const passwordRef = ref() +const formRef = ref() + +const handleEmailInputEvent = (newValue: any) => { + emailRef.value = newValue + console.log(emailRef.value) +} + +const handlePasswordInputEvent = (newValue: any) => { + passwordRef.value = newValue +} const handleSubmit = () => { + formRef.value.classList.add("was-validated") alert("Expected to be logged in when backend are finished") // Todo remove this line } + </script> <template> <div class="container-fluid"> - <form id="loginForm" @submit.prevent="handleSubmit"> - <BaseInput id="usernameInput" - input-id="username" - type="text" - label="Username" - placeholder="Enter username"/> - <BaseInput id="passwordInput" + <form ref="formRef" id="loginForm" @submit.prevent="handleSubmit"> + <BaseInput :model-value="emailRef" + @input-change-event="handleEmailInputEvent" + id="emailInput" + input-id="email" + type="email" + label="Email" + placeholder="Enter your email"/> + <BaseInput :model-value="passwordRef" + @input-change-event="handlePasswordInputEvent" + id="passwordInput" input-id="password" type="password" label="Password" placeholder="Enter password"/> - <button1 id="confirmButton" @click="handleSubmit" button-text="Login"></button1> + <button1 id="confirmButton" type="submit" @click="handleSubmit" button-text="Login"></button1> </form> </div> </template> <style scoped> .container-fluid { - height: 91vh; - display: grid; - justify-items: center; - align-items: center; -} - -#usernameInput, #passwordInput, #confirmButton { - margin: 15px 0; -} - -#confirmButton { - justify-content: center; + max-width: 450px; } #loginForm { display: flex; flex-direction: column; - min-width: 280px; - width: 40%; + justify-items: center; +} + +#emailInput, #passwordInput, #confirmButton { + margin: 1rem 0; } </style> \ No newline at end of file diff --git a/src/components/SavingGoalComponents/SavingGoalRoadmap.vue b/src/components/SavingGoalComponents/SavingGoalRoadmap.vue new file mode 100644 index 0000000000000000000000000000000000000000..89f6587d79ed679fcd92d02af1d2d3c5dc31aef9 --- /dev/null +++ b/src/components/SavingGoalComponents/SavingGoalRoadmap.vue @@ -0,0 +1,372 @@ +<script lang="ts"> +interface Step { + image: string; + title: string; + showPanel: boolean; + description: string; + percentFinished: number; +} +export default { + data() { + return { + //This will be changed to info gathered from backend + steps: [ + { image: 'https://th.bing.com/th/id/OIG4.GHQvXMljb6YONcnYvfuE?pid=ImgGn', altImage: 'https://th.bing.com/th/id/OIG2.dzSCGYTFEglaUPj1maja?pid=ImgGn', title: 'Challenge 1', showPanel: false, description: 'Save 50kr on coffee in 3 days', percentFinished: 22 }, + { image: 'https://th.bing.com/th/id/OIG4.GHQvXMljb6YONcnYvfuE?pid=ImgGn', altImage: 'https://th.bing.com/th/id/OIG2.dzSCGYTFEglaUPj1maja?pid=ImgGn', title: 'Challenge 2', showPanel: false, description: 'Save 500kr on food in 7 days', percentFinished: 73 }, + { image: 'https://th.bing.com/th/id/OIG4.GHQvXMljb6YONcnYvfuE?pid=ImgGn', altImage: 'https://th.bing.com/th/id/OIG2.dzSCGYTFEglaUPj1maja?pid=ImgGn', title: 'Challenge 3', showPanel: false, description: 'Save 350kr on clothes in 5 days', percentFinished: 50 }, + { image: 'https://th.bing.com/th/id/OIG4.GHQvXMljb6YONcnYvfuE?pid=ImgGn', altImage: 'https://th.bing.com/th/id/OIG2.dzSCGYTFEglaUPj1maja?pid=ImgGn', title: 'Challenge 4', showPanel: false, description: 'Save 150kr on coffee in 4 days', percentFinished: 10 }, + { image: 'https://th.bing.com/th/id/OIG4.GHQvXMljb6YONcnYvfuE?pid=ImgGn', altImage: 'https://th.bing.com/th/id/OIG2.dzSCGYTFEglaUPj1maja?pid=ImgGn', title: 'Challenge 5', showPanel: false, description: 'Save 50kr on coffee in 3 days', percentFinished: 90 } + ] + }; + }, + methods: { + togglePanel(step: Step) { + step.showPanel = !step.showPanel; + } + } +}; +</script> + +<template> + <div class="container"> + <div class="row"> + <div class="col-lg-12"> + <ul class="timeline"> + <li v-for="(step, index) in steps" :key="index" :class="{ 'timeline-inverted': index % 2 !== 0 }"> + <div class="timeline-image" @click="togglePanel(step)"> + <img class="circular-image" :src="step.showPanel ? step.altImage : step.image" alt=""> + </div> + <div class="timeline-panel" v-show="step.showPanel"> + <div class="timeline-heading"> + <h4>{{ step.title }}</h4> + <h4 class="subheading">{{step.description}}</h4> + </div> + <div class="timeline-body"> + <br> + <p class=""> + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + </p> + <br> + <div class="progress"> + <div class="progress-bar bg-success" role="progressbar" :style="{ width: step.percentFinished + '%' }" :aria-valuenow="step.percentFinished" aria-valuemin="0" aria-valuemax="100"></div> + </div> + <br> + <div class="form-check form-check-inline"> + <input class="form-check-input" type="checkbox" id="inlineCheckbox1" value="option1"> + <label class="form-check-label" style="color:white;" for="inlineCheckbox1">Day 1</label> + </div> + <div class="form-check form-check-inline"> + <input class="form-check-input" type="checkbox" id="inlineCheckbox2" value="option2"> + <label class="form-check-label" style="color:white;" for="inlineCheckbox1">Day 2</label> + </div> + </div> + </div> + <div class="line" v-if="index < steps.length - 1"></div> + </li> + </ul> + </div> + </div> + </div> +</template> + +<style scoped> +.container { + margin-bottom:20px; +} + +.timeline { + position: relative; + padding:4px 0 0 0; + margin-top:22px; + list-style: none; +} + +.timeline>li:nth-child(even) { + position: relative; + margin-bottom: 50px; + height: 180px; + right:-100px; +} + +.timeline>li:nth-child(odd) { + position: relative; + margin-bottom: 50px; + height: 180px; + left:-100px; +} + +.timeline>li:before, +.timeline>li:after { + content: " "; + display: table; +} + +.timeline>li:after { + clear: both; + min-height: 170px; +} + +.timeline > li .timeline-panel { + position: relative; + float: left; + width: 41%; + padding: 0 20px 20px 30px; + text-align: right; + background-color: #32CD32; + border-radius: 1em; +} + +.timeline>li .timeline-panel:before { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; +} + +.timeline>li .timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; +} + +.timeline>li .timeline-image { + z-index: 100; + position: absolute; + left: 50%; + border: 7px solid #006400; + border-radius: 100%; + background-color: #00FF7F; + box-shadow: 0 0 5px #00FF00; + width: 200px; + height: 200px; + margin-left: -100px; + cursor:pointer; +} + +.timeline>li .timeline-image h4 { + margin-top: 12px; + font-size: 10px; + line-height: 14px; +} + +.timeline>li.timeline-inverted>.timeline-panel { + float: right; + padding: 0 30px 20px 20px; + text-align: left; +} + +.timeline>li.timeline-inverted>.timeline-panel:before { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; +} + +.timeline>li.timeline-inverted>.timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; +} + +.timeline>li:last-child { + margin-bottom: 0; +} + +.timeline .timeline-heading h4 { + margin-top:22px; + margin-bottom: 4px; + padding:0; + color: white; + font-weight:600; +} + +.timeline .timeline-heading h4.subheading { + margin:0; + padding:0; + text-transform: none; + font-size:18px; + color:white; +} + +.timeline .timeline-body>p, +.timeline .timeline-body>ul { + margin-bottom: 0; + color:white; +} +/*Style for even div.line*/ +.timeline>li:nth-child(odd) .line:before { + content: ""; + position: absolute; + top: 60px; + bottom: 0; + left: 730px; + width: 45px; + height:340px; + background-color: grey; + -ms-transform: rotate(-44deg); /* IE 9 */ + -webkit-transform: rotate(-44deg); /* Safari */ + transform: rotate(-44deg); + border: dotted white 3px; + /**box-shadow: 0 0 5px #00FF00;**/ +} +/*Style for odd div.line*/ +.timeline>li:nth-child(even) .line:before { + content: ""; + position: absolute; + top: 60px; + bottom: 0; + left: 520px; + width: 45px; + height:340px; + background-color: grey; + -ms-transform: rotate(44deg); /* IE 9 */ + -webkit-transform: rotate(44deg); /* Safari */ + transform: rotate(44deg); + border: dotted white 3px; + /*box-shadow: 0 0 5px #00FF00;*/ +} +/* Medium Devices, .visible-md-* */ +@media (min-width: 992px) and (max-width: 1199px) { + .timeline > li:nth-child(even) { + margin-bottom: 0; + min-height: 0; + right: 0; + } + .timeline > li:nth-child(odd) { + margin-bottom: 0; + min-height: 0; + left: 0; + } + .timeline>li:nth-child(even) .timeline-image { + left: 0; + margin-left: 0; + } + .timeline>li:nth-child(odd) .timeline-image { + left: 690px; + margin-left: 0; + } + .timeline > li:nth-child(even) .timeline-panel { + width: 76%; + padding: 0 0 20px 0; + text-align: left; + } + .timeline > li:nth-child(odd) .timeline-panel { + width: 70%; + padding: 0 0 20px 0; + text-align: right; + } + .timeline > li .line { + display: none; + } +} +/* Small Devices, Tablets */ +@media (min-width: 768px) and (max-width: 991px) { + .timeline > li:nth-child(even) { + margin-bottom: 0; + min-height: 0; + right: 0; + } + .timeline > li:nth-child(odd) { + margin-bottom: 0; + min-height: 0; + left: 0; + } + .timeline>li:nth-child(even) .timeline-image { + left: 0; + margin-left: 0; + } + .timeline>li:nth-child(odd) .timeline-image { + left: 520px; + margin-left: 0; + } + .timeline > li:nth-child(even) .timeline-panel { + width: 70%; + padding: 0 0 20px 0; + text-align: left; + } + .timeline > li:nth-child(odd) .timeline-panel { + width: 70%; + padding: 0 0 20px 0; + text-align: right; + } + .timeline > li .line { + display: none; + } +} +/* Custom, iPhone Retina */ +@media only screen and (max-width: 767px) { + .timeline > li:nth-child(even) { + margin-bottom: 0; + min-height: 0; + right: 0; + } + .timeline > li:nth-child(odd) { + margin-bottom: 0; + min-height: 0; + left: 0; + } + .timeline>li .timeline-image { + position: static; + width: 150px; + height: 150px; + margin-bottom:0; + } + .timeline>li:nth-child(even) .timeline-image { + left: 0; + margin-left: 0; + } + .timeline>li:nth-child(odd) .timeline-image { + float:right; + left: 0; + margin-left:0; + } + .timeline > li:nth-child(even) .timeline-panel { + width: 100%; + padding: 0 0 20px 14px; + } + .timeline > li:nth-child(odd) .timeline-panel { + width: 100%; + padding: 0 14px 20px 0; + } + .timeline > li .line { + display: none; + } +} + +/* Additional styles for inverted timeline panels */ +.timeline > li.timeline-inverted:nth-child(even) .timeline-panel { + float: right; + padding: 0 30px 20px 20px; + text-align: left; +} + +.timeline > li.timeline-inverted:nth-child(odd) .timeline-panel { + float: left; + padding: 0 20px 20px 30px; + text-align: right; +} + +.timeline > li.timeline-inverted:nth-child(even) .timeline-panel:before, +.timeline > li.timeline-inverted:nth-child(even) .timeline-panel:after { + right: auto; + left: -15px; + border-right-width: 15px; + border-left-width: 0; +} + +.timeline > li.timeline-inverted:nth-child(odd) .timeline-panel:before, +.timeline > li.timeline-inverted:nth-child(odd) .timeline-panel:after { + right: auto; + left: -14px; + border-right-width: 14px; + border-left-width: 0; +} + +.circular-image { + border-radius: 50%; + max-width: 100%; + height: auto; +} +</style> \ No newline at end of file diff --git a/src/components/SignUp/SignUp.vue b/src/components/SignUp/SignUp.vue new file mode 100644 index 0000000000000000000000000000000000000000..4dff39dcd4bbe2a47e1471473d7a1632eb540827 --- /dev/null +++ b/src/components/SignUp/SignUp.vue @@ -0,0 +1,12 @@ +<script setup lang="ts"> + +import SignUpForm from '@/components/SignUp/SignUpForm.vue' +</script> + +<template> + <SignUpForm/> +</template> + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/components/SignUp/SignUpForm.vue b/src/components/SignUp/SignUpForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..04a8547844c7261753afb61f1d9323c9d773d889 --- /dev/null +++ b/src/components/SignUp/SignUpForm.vue @@ -0,0 +1,105 @@ +<script setup lang="ts"> +import BaseInput from '@/components/InputFields/BaseInput.vue' +import Button1 from '@/components/Buttons/Button1.vue' +import { ref } from 'vue' + +const firstNameRef = ref('') +const surnameRef = ref('') +const emailRef = ref('') +const passwordRef = ref('') +const confirmPasswordRef = ref('') +const formRef = ref() + +const handleFirstNameInputEvent = (newValue: any) => { + firstNameRef.value = newValue + console.log(firstNameRef.value) +} + +const handleSurnameInputEvent = (newValue: any) => { + surnameRef.value = newValue +} + +const handleEmailInputEvent = (newValue: any) => { + emailRef.value = newValue +} + +const handlePasswordInputEvent = (newValue: any) => { + passwordRef.value = newValue +} + +const handleConfirmPasswordInputEvent = (newValue: any) => { + confirmPasswordRef.value = newValue +} + +const handleSubmit = () => { + formRef.value.classList.add("was-validated") + alert("Expected to be transferred to initial configuration") // Todo remove this line +} + +</script> + +<template> + <div class="container"> + <form ref="formRef" id="signUpForm" @submit.prevent="handleSubmit"> + <BaseInput :model-value="firstNameRef.value" + @input-change-event="handleFirstNameInputEvent" + ref="firstNameRef" + id="firstNameInput" + input-id="first-name" + type="text" + label="First name" + placeholder="Enter your first name"/> + <BaseInput :model-value="surnameRef.value" + @input-change-event="handleSurnameInputEvent" + ref="surnameRef" + id="surnameInput" + input-id="surname" + type="text" + label="Surname" + placeholder="Enter your surname"/> + <BaseInput :model-value="emailRef.value" + @input-change-event="handleEmailInputEvent" + ref="emailRef" + id="emailInput" + input-id="email" + type="email" + label="Email" + placeholder="Enter your email"/> + <BaseInput :model-value="passwordRef.value" + @input-change-event="handlePasswordInputEvent" + ref="passwordRef" + id="passwordInput" + input-id="password" + type="password" + label="Password" + placeholder="Enter password"/> + <BaseInput :model-value="confirmPasswordRef.value" + @input-change-event="handleConfirmPasswordInputEvent" + ref="confirmPasswordRef" + id="confirmPasswordInput" + input-id="confirmPassword" + type="password" + label="Confirm Password" + placeholder="Confirm password"/> + <button1 id="confirmButton" @click="handleSubmit" button-text="Sign up"></button1> + </form> + </div> + +</template> + +<style scoped> + +.container { + max-width: 450px; +} + +#signUpForm { + display: flex; + flex-direction: column; + justify-items: center; +} + +#firstNameInput, #surnameInput, #emailInput, #passwordInput, #confirmButton, #confirmPasswordInput { + margin: 1rem 0; +} +</style> \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 33f8b29fb3109eb4e12723fbd350178a6439d0cd..6a74b73269b5fbf36e200dde9b9b86e64b27cafa 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -3,6 +3,8 @@ import { createRouter, createWebHistory } from 'vue-router'; import LoginView from '../views/Authentication/LoginView.vue'; import { useUserInfoStore } from '@/stores/UserStore'; import UserProfileView from "@/views/User/UserProfileView.vue"; +import SignUp from '@/components/SignUp/SignUp.vue' + const routes = [ { @@ -31,6 +33,11 @@ const routes = [ name: 'test', component: () => import('@/views/TestView.vue'), }, + { + path: 'roadmap', + name: 'roadmap', + component: () => import('@/views/SavingGoalView/RoadmapView.vue'), + }, { path: 'admin', name: 'admin', @@ -54,6 +61,11 @@ const routes = [ name: 'profile', component: UserProfileView }, + { + path: '/sign-up', + name: 'sign up', + component: () => import('@/views/Authentication/SignUpView.vue'), + }, { path: '/:pathMatch(.*)*', redirect: { name: 'not-found' }, diff --git a/src/views/Authentication/SignUpView.vue b/src/views/Authentication/SignUpView.vue new file mode 100644 index 0000000000000000000000000000000000000000..ce0bd3ada3a372fbae632554d2e28bf02f92e162 --- /dev/null +++ b/src/views/Authentication/SignUpView.vue @@ -0,0 +1,15 @@ +<script setup lang="ts"> +import Footer from '@/components/BaseComponents/Footer.vue' +import Menu from '@/components/BaseComponents/Menu.vue' +import SignUp from '@/components/SignUp/SignUp.vue' +</script> + +<template> + <Menu/> + <SignUp/> + <Footer/> +</template> + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index f30d4244a9ec682c7a3c4d1c8a9bc3575779e181..70c5e6456e7f882ebbea3fa1476b4cae2fb52aa2 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -3,5 +3,7 @@ </script> <template> - <RouterLink to="login">Login</RouterLink> + <RouterLink to="login">Login</RouterLink> + <br/> + <RouterLink to="sign-up">Sign up</RouterLink> </template> diff --git a/src/views/LeaderboardView.vue b/src/views/LeaderboardView.vue new file mode 100644 index 0000000000000000000000000000000000000000..7f58d1768c051c4c1b780a40ba89e02f88e6964d --- /dev/null +++ b/src/views/LeaderboardView.vue @@ -0,0 +1,48 @@ +<template> + <main> + <div id="leaderboard"> + <h1>Ranking</h1> + <Leaderboard :leaderboard="leaderboardData" @navigateToUserProfile="navigateToUserProfile" /> + </div> + </main> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { useRoute, useRouter } from 'vue-router'; +import Leaderboard from '@/components/LeaderboardComponents/Leaderboard.vue'; + +let leaderboardData = ref([]); + +const router = useRouter(); + +async function fetchQuizData() { + /*leaderboard(quizId).then((response) => { + leaderboardData.value = response.data.slice(0, 10); + }).catch((error) => { + console.error("Failed to fetch leaderboard data:", error); + });*/ +} + +const navigateToUserProfile = (userId: number) => { + router.push({ name: 'user', params: { id: userId } }); +}; +</script> + +<style scoped> +main { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +#leaderboard { + width: 60%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-bottom: 3rem; +} +</style> \ No newline at end of file diff --git a/src/views/SavingGoalView/RoadmapView.vue b/src/views/SavingGoalView/RoadmapView.vue new file mode 100644 index 0000000000000000000000000000000000000000..17c812a5d70eebc4abe899b255f9522f4dc663f8 --- /dev/null +++ b/src/views/SavingGoalView/RoadmapView.vue @@ -0,0 +1,7 @@ +<script setup lang="ts"> +import SavingGoalRoadmap2 from "@/components/SavingGoalComponents/SavingGoalRoadmap.vue"; +</script> + +<template> +<saving-goal-roadmap2></saving-goal-roadmap2> +</template> \ No newline at end of file