Added login and register view and removed vue boilerplate
parent
8ba822814b
commit
c9cde25d3b
@ -0,0 +1 @@
|
||||
VITE_BASE_API_URL=/api
|
||||
@ -1,15 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import NavBar from './components/NavBar.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavBar/>
|
||||
<NavBar />
|
||||
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "noto-sans";
|
||||
src: url("./assets/elevio-noto-sans-400.woff");
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "noto-sans", Verdana, Geneva, sans-serif;
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
@ -1,24 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const loginStore = useLoginStore();
|
||||
const { loggedIn } = storeToRefs(loginStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="NavBar">
|
||||
<img alt="Fitness World Logo" class="logo" src="@/assets/logo.svg" width="125" />
|
||||
<div class="links">
|
||||
<RouterLink to="/about">test</RouterLink>
|
||||
<div class="NavBar">
|
||||
<div class="NavBarContent">
|
||||
<RouterLink to="/">
|
||||
<img alt="Fitness World Logo" class="logo" src="@/assets/logo.svg" width="125" />
|
||||
</RouterLink>
|
||||
<div class="links">
|
||||
<!-- <RouterLink to="/center">Fitness center</RouterLink>
|
||||
<RouterLink to="/trainer">Personlig træner</RouterLink> -->
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<RouterLink to="/login" v-if="!loggedIn">Login</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.NavBar {
|
||||
width: 100vw;
|
||||
position: absolute;
|
||||
/* position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: 0; */
|
||||
}
|
||||
|
||||
.NavBarContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
margin: 0px 10px;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,9 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
import "bootstrap/dist/js/bootstrap.js";
|
||||
@ -0,0 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useLoginStore = defineStore('login', {
|
||||
state: () => {
|
||||
return { loggedIn: false }
|
||||
},
|
||||
actions: {
|
||||
setLoginState(login: boolean) {
|
||||
this.loggedIn = login;
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// import TheWelcome from '../components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
data() {
|
||||
return {
|
||||
email: "",
|
||||
emailBlured: false,
|
||||
emailError: "",
|
||||
password: "",
|
||||
passwordBlured: false,
|
||||
passwordError: ""
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const loginStore = useLoginStore();
|
||||
const { setLoginState } = loginStore;
|
||||
|
||||
return { setLoginState };
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.emailError = "";
|
||||
this.passwordError = "";
|
||||
|
||||
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/login`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ password: this.password, email: this.email }),
|
||||
headers: {
|
||||
"Content-Type": "Application/json"
|
||||
}
|
||||
});
|
||||
if (res.status === 400) {
|
||||
const errors = await res.json();
|
||||
console.log(errors);
|
||||
for (const error of errors) {
|
||||
if (error.path?.[0] === "email") {
|
||||
this.emailError = error.message;
|
||||
}
|
||||
else if (error.path?.[0] === "password") {
|
||||
this.passwordError = error.message;
|
||||
}
|
||||
else if (error.type === "login.invalid") {
|
||||
this.passwordError = "The username or password was incorrect";
|
||||
}
|
||||
}
|
||||
} else if (res.status === 204) {
|
||||
this.setLoginState(true);
|
||||
if (this.$route.query.ref) {
|
||||
this.$router.push({ path: this.$route.query.ref.toString() });
|
||||
} else {
|
||||
this.$router.push({ path: '/' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mt-5">
|
||||
<div class="row d-flex justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card px-5 py-5" id="form1">
|
||||
<div class="form-data">
|
||||
<div class="forms-inputs mb-4">
|
||||
<span>Email</span>
|
||||
<input autocomplete="off" type="email" v-model="email"
|
||||
v-bind:class="{ 'form-control': true, 'is-invalid': emailError }"
|
||||
v-on:blur="emailError = ''" v-on:keypress="if ($event.key === 'Enter') submit();">
|
||||
<div class="invalid-feedback">{{ emailError }}</div>
|
||||
</div>
|
||||
<div class="forms-inputs mb-4">
|
||||
<span>Password</span>
|
||||
<input autocomplete="off" type="password" v-model="password"
|
||||
v-bind:class="{ 'form-control': true, 'is-invalid': passwordError }"
|
||||
v-on:blur="passwordError = ''" v-on:keypress="if ($event.key === 'Enter') submit();">
|
||||
<div class="invalid-feedback">{{ passwordError }}</div>
|
||||
</div>
|
||||
<RouterLink class="mb-3" to="/register">
|
||||
<p>Don't have an account yet?</p>
|
||||
</RouterLink>
|
||||
<div class="mb-3">
|
||||
<button v-on:click.stop.prevent="submit" class="btn btn-dark w-100">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
height: 320px
|
||||
}
|
||||
|
||||
.forms-inputs {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.forms-inputs span {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: 10px;
|
||||
background-color: #fff;
|
||||
padding: 5px 10px;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.forms-inputs input {
|
||||
height: 50px;
|
||||
border: 2px solid #eee
|
||||
}
|
||||
|
||||
.forms-inputs input:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
border: 2px solid #000
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 50px
|
||||
}
|
||||
|
||||
.success-data {
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.bxs-badge-check {
|
||||
font-size: 90px
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
|
||||
export default {
|
||||
name: "Register",
|
||||
data() {
|
||||
return {
|
||||
firstname: "",
|
||||
firstnameError: "",
|
||||
lastname: "",
|
||||
lastnameError: "",
|
||||
email: "",
|
||||
emailError: "",
|
||||
password: "",
|
||||
passwordError: ""
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const loginStore = useLoginStore();
|
||||
const { setLoginState } = loginStore;
|
||||
|
||||
return { setLoginState };
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.emailError = "";
|
||||
this.passwordError = "";
|
||||
|
||||
const res = await fetch(`${import.meta.env.VITE_BASE_API_URL}/register`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
password: this.password,
|
||||
email: this.email,
|
||||
firstname: this.firstname,
|
||||
lastname: this.lastname
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "Application/json"
|
||||
}
|
||||
});
|
||||
if (res.status === 400) {
|
||||
const errors = await res.json();
|
||||
console.log(errors);
|
||||
for (const error of errors) {
|
||||
if (error.path?.[0] === "email") {
|
||||
this.emailError = error.message;
|
||||
}
|
||||
else if (error.path?.[0] === "password") {
|
||||
this.passwordError = error.message;
|
||||
}
|
||||
else if (error.path?.[0] === "firstname") {
|
||||
this.firstnameError = error.message;
|
||||
}
|
||||
else if (error.path?.[0] === "lastname") {
|
||||
this.lastnameError = error.message;
|
||||
}
|
||||
}
|
||||
} else if (res.status === 204) {
|
||||
if (this.$route.query.ref) {
|
||||
this.$router.push({ path: this.$route.query.ref.toString() });
|
||||
} else {
|
||||
this.$router.push({ path: '/' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mt-5">
|
||||
<div class="row d-flex justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card px-5 py-5" id="form1">
|
||||
<div class="form-data">
|
||||
<div class="forms-inputs mb-4">
|
||||
<span>Firstname</span>
|
||||
<input autocomplete="off" type="text" v-model="firstname"
|
||||
v-bind:class="{ 'form-control': true, 'is-invalid': firstnameError }"
|
||||
v-on:blur="firstnameError = ''" v-on:keypress="if ($event.key === 'Enter') submit();">
|
||||
<div class="invalid-feedback">{{ firstnameError }}</div>
|
||||
</div>
|
||||
<div class="forms-inputs mb-4">
|
||||
<span>Lastname</span>
|
||||
<input autocomplete="off" type="text" v-model="lastname"
|
||||
v-bind:class="{ 'form-control': true, 'is-invalid': lastnameError }"
|
||||
v-on:blur="lastnameError = ''" v-on:keypress="if ($event.key === 'Enter') submit();">
|
||||
<div class="invalid-feedback">{{ lastnameError }}</div>
|
||||
</div>
|
||||
<div class="forms-inputs mb-4">
|
||||
<span>Email</span>
|
||||
<input autocomplete="off" type="email" v-model="email"
|
||||
v-bind:class="{ 'form-control': true, 'is-invalid': emailError }"
|
||||
v-on:blur="emailError = ''" v-on:keypress="if ($event.key === 'Enter') submit();">
|
||||
<div class="invalid-feedback">{{ emailError }}</div>
|
||||
</div>
|
||||
<div class="forms-inputs mb-4">
|
||||
<span>Password</span>
|
||||
<input autocomplete="off" type="password" v-model="password"
|
||||
v-bind:class="{ 'form-control': true, 'is-invalid': passwordError }"
|
||||
v-on:blur="passwordError = ''" v-on:keypress="if ($event.key === 'Enter') submit();">
|
||||
<div class="invalid-feedback">{{ passwordError }}</div>
|
||||
</div>
|
||||
<RouterLink class="mb-3" to="/login">
|
||||
<p>Already have an account?</p>
|
||||
</RouterLink>
|
||||
<div class="mb-3">
|
||||
<button v-on:click.stop.prevent="submit" class="btn btn-dark w-100">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
.card {
|
||||
border: none;
|
||||
height: 320px
|
||||
}
|
||||
|
||||
.forms-inputs {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.forms-inputs span {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
left: 10px;
|
||||
background-color: #fff;
|
||||
padding: 5px 10px;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.forms-inputs input {
|
||||
height: 50px;
|
||||
border: 2px solid #eee
|
||||
}
|
||||
|
||||
.forms-inputs input:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
border: 2px solid #000
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 50px
|
||||
}
|
||||
|
||||
.success-data {
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.bxs-badge-check {
|
||||
font-size: 90px
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue