This commit is contained in:
Hubert Van De Walle 2020-07-03 23:51:10 +02:00
parent 9ab5a10816
commit 6a0a580d2f
12 changed files with 215 additions and 379 deletions

View File

@ -1,68 +0,0 @@
<template>
<v-card flat>
<v-form ref="form" v-model="valid">
<v-card-text>
<v-text-field
v-model="form.username"
:rules="usernameRules"
label="Username"
required
:prepend-icon="mdiAccount"
></v-text-field>
<v-text-field
v-model="form.password"
:rules="passwordRules"
label="Password"
required
:prepend-icon="mdiLock"
type="password"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
:disabled="!valid"
color="success"
type="submit"
@click.prevent="userLogin"
>
Login
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script>
import { mdiLock, mdiAccount } from '@mdi/js'
export default {
name: 'LoginForm',
data: () => ({
mdiLock,
mdiAccount,
valid: false,
form: {
username: '',
password: '',
},
usernameRules: [(v) => !!v || 'Name is required'],
passwordRules: [(v) => !!v || 'Password is required'],
}),
methods: {
userLogin() {
this.$auth
.loginWith('local', {
data: this.form,
})
.then((_) =>
this.$root.$emit('toast', 'Welcome back', 'success')
)
.catch((_) =>
this.$root.$emit('toast', 'Invalid credentials', 'error')
)
},
},
}
</script>

View File

@ -1,122 +0,0 @@
<template>
<div>
<v-navigation-drawer v-model="drawer" clipped fixed app color="primary">
<client-only>
<v-list-item v-if="loggedIn">
<v-list-item-avatar>
<v-icon>{{ mdiAccount }}</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ $auth.$state.user.username }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list>
<v-list-item
v-for="(item, i) in items"
:key="i"
:to="item.to"
router
nuxt
exact
>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title v-text="item.title" />
</v-list-item-content>
</v-list-item>
</v-list>
</client-only>
<template v-slot:append>
<client-only>
<div class="pa-2">
<v-btn
v-if="loggedIn"
block
color="secondary"
@click.stop="logout"
>
Logout
</v-btn>
<v-btn v-else block color="secondary" to="/account">
Login
</v-btn>
</div>
</client-only>
</template>
</v-navigation-drawer>
<v-app-bar clipped-left fixed app color="primary">
<v-app-bar-nav-icon @click.stop="drawer = !drawer">
<v-icon>{{ mdiMenu }}</v-icon>
</v-app-bar-nav-icon>
<v-btn to="/" text rounded>Simple Notes</v-btn>
<v-spacer />
<client-only>
<v-btn aria-label="theme switcher" icon @click="toggleTheme">
<v-icon>
{{
$vuetify.theme.dark
? mdiBrightness2
: mdiBrightness5
}}
</v-icon>
</v-btn>
</client-only>
</v-app-bar>
</div>
</template>
<script>
import {
mdiMenu,
mdiBrightness5,
mdiBrightness2,
mdiPencil,
mdiHome,
mdiAccount,
} from '@mdi/js'
import { mapState } from 'vuex'
export default {
name: 'Navbar',
data: () => ({
mdiMenu,
mdiBrightness5,
mdiBrightness2,
mdiAccount,
drawer: false,
items: [
{
icon: mdiHome,
title: 'Welcome',
to: '/',
},
{
icon: mdiPencil,
title: 'Notes',
to: '/notes',
},
],
}),
computed: {
...mapState('auth', ['loggedIn']),
},
methods: {
async logout() {
this.$store.commit('notes/clear')
await this.$auth.logout()
},
toggleTheme() {
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
const theme = this.$vuetify.theme.dark ? 'dark' : 'light'
localStorage.setItem('theme', theme)
},
},
}
</script>

View File

@ -1,48 +0,0 @@
<template>
<v-card class="d-flex flex-column" height="100%">
<v-card-title>
<h2 class="title primary--text">{{ title }}</h2>
</v-card-title>
<v-card-text> Last updated {{ prettyDate }} </v-card-text>
<v-card-actions>
<TagsGroup :tags="tags" />
<v-spacer />
<v-btn text color="accent" @click="$router.push(`/notes/${uuid}`)"
>View</v-btn
>
</v-card-actions>
</v-card>
</template>
<script>
import { format } from 'timeago.js'
import TagsGroup from '@/components/TagsGroup'
export default {
name: 'NoteCard',
components: { TagsGroup },
props: {
uuid: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
tags: {
type: Array,
default: () => [],
},
updatedAt: {
type: String,
default: null,
},
},
computed: {
prettyDate() {
return format(this.updatedAt)
},
},
}
</script>

View File

@ -1,96 +0,0 @@
<template>
<v-card flat>
<v-form ref="form" v-model="valid">
<v-card-text>
<v-text-field
v-model="form.username"
:rules="usernameRules"
label="Username"
required
:prepend-icon="mdiAccount"
></v-text-field>
<v-text-field
v-model="form.password"
:rules="passwordRules"
label="Password"
required
:prepend-icon="mdiLock"
type="password"
></v-text-field>
<v-text-field
v-model="confirm"
:rules="confirmRules"
label="Confirm your password"
required
:prepend-icon="mdiLock"
type="password"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
:disabled="!valid"
color="success"
type="submit"
@click.prevent="registerUser"
>
Register
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<script>
import { mdiAccount, mdiAt, mdiLock } from '@mdi/js'
export default {
name: 'RegisterForm',
data: (vm) => ({
mdiLock,
mdiAccount,
mdiAt,
valid: false,
form: {
username: '',
password: '',
},
usernameRules: [
(v) => !!v || 'Username is required',
(v) =>
v.length <= 50 ||
'Username must be shorter or equal to 50 characters',
(v) =>
v.length >= 3 ||
'Username must be longer or equal to 3 characters',
],
passwordRules: [
(v) => !!v || 'Password is required',
(v) =>
v.length >= 6 ||
'Password must be longer or equal to 6 characters',
],
confirm: '',
confirmRules: [
(v) => !!v || 'Password is required',
(v) => v === vm.form.password || 'Both passwords must be equals',
],
}),
methods: {
registerUser() {
this.$axios
.post('/user', this.form)
.then(() => this.$root.$emit('register::success'))
.catch(() =>
this.$root.$emit(
'toast',
'Please choose another username',
'error'
)
)
},
},
}
</script>

View File

@ -1,32 +0,0 @@
<template>
<div class="tags">
<v-chip
v-for="(tag, index) in tags"
:key="index"
active
color="secondary"
>
{{ tag }}
</v-chip>
</div>
</template>
<script>
export default {
name: 'TagsGroup',
props: {
tags: {
type: Array,
required: true,
},
},
}
</script>
<style scoped>
.tags {
padding: 16px;
}
.tags .v-chip {
margin: 4px 8px 4px 0;
}
</style>

View File

@ -1,5 +1,11 @@
<template>
<div class="h-screen bg-gray-900 font-sans text-gray-100">
<div class="h-screen font-sans text-gray-100">
<nuxt />
</div>
</template>
<style>
html {
@apply bg-gray-900;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<v-app dark>
<div>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
@ -9,7 +9,7 @@
<NuxtLink to="/">
Home page
</NuxtLink>
</v-app>
</div>
</template>
<script>

View File

@ -68,7 +68,7 @@ export default ({ command }) => ({
auth: {
redirect: {
login: '/account',
login: '/signin',
logout: '/',
home: '/notes',
},

View File

@ -1,15 +1,42 @@
<template>
<div>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet,
asperiores assumenda dolor ea esse eum itaque iure libero magni, nam
porro, quas reiciendis repellat soluta sunt! Accusantium eum omnis
quasi.
<nav>
<ul>
<li class="text-blue-200">
<router-link class="underline" to="/signin"
>Sign In</router-link
>
</li>
<li class="text-blue-200">
<router-link class="underline" to="/register"
>Register</router-link
>
</li>
<li class="text-blue-200">
<button class="underline" @click="logout">Sign Out</button>
</li>
</ul>
</nav>
<client-only>
<div v-if="$auth.$state.loggedIn">
User: {{ $auth.$state.user }}
</div>
</client-only>
</div>
</template>
<script>
export default {
name: 'Home',
options: {
auth: false, // FIXME: auth: 'guest'
},
methods: {
async logout() {
this.$store.commit('notes/clear')
await this.$auth.logout()
},
},
}
</script>

42
frontend/pages/notes.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<div class="container p-4 mx-auto bg-gray-700">
<h1 class="text-center">My Notes</h1>
<input
id="search"
name="search"
class="block p-2 my-2 appearance-none w-full bg-transparent border-white-200 border-b focus:border-red-500"
placeholder="search"
aria-label="search"
/>
<div v-for="note in notes" :key="note.uuid">
<div class="flex justify-between items-center">
<h2 class="text-lg">{{ note.title }}</h2>
<div class="py-2">
<span
v-for="(tag, index) in note.tags"
:key="index"
class="inline-block text-sm bg-gray-500 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 ml-2"
>{{ tag }}</span
>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
name: 'Notes',
computed: {
...mapState('notes', ['notes', 'isInitialized']),
},
mounted() {
this.load()
},
methods: {
...mapActions('notes', ['load']),
},
}
</script>

127
frontend/pages/register.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<div
class="h-screen container mx-auto h-full flex justify-center items-center"
>
<div class="w-full md:w-1/2 lg:w-1/3">
<h1 class="font-semibold text-lg mb-6 text-center">
Create an Account
</h1>
<div
class="bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
>
<form @submit.prevent="register">
<div
v-if="showError"
class="bg-red-500 border-l-4 border-red-700 text-red-200 p-4 mb-4"
role="alert"
>
<p class="font-bold">Error</p>
<p>{{ error }}</p>
</div>
<div class="mb-4">
<label
for="username"
class="font-bold text-grey-darker block mb-2"
>
Username
</label>
<input
id="username"
v-model="form.username"
name="username"
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
placeholder="Your Username"
/>
</div>
<div class="mb-4">
<label
for="password"
class="font-bold text-grey-darker block mb-2"
>
Password
</label>
<input
id="password"
v-model="form.password"
name="password"
type="password"
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
placeholder="Your Password"
/>
</div>
<div class="mb-4">
<label
for="confirm"
class="font-bold text-grey-darker block mb-2"
>
Confirm your password
</label>
<input
id="confirm"
v-model="confirm"
name="confirm"
type="password"
class="shadow focus:shadow-outline block appearance-none w-full bg-gray-700 border-gray-500 hover:border-gray-500 px-2 py-2 rounded shadow"
placeholder="Confirm your Password"
/>
</div>
<div class="flex items-center mt-6">
<button
type="submit"
class="w-full bg-teal-500 hover:bg-teal-400 text-white font-bold py-2 px-4 rounded"
>
Sign In
</button>
</div>
</form>
</div>
<div class="text-center">
<p class="text-gray-200 text-sm">
Already have an account?
<nuxt-link
to="/signin"
class="no-underline text-blue-500 font-bold"
>Sign in
</nuxt-link>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SignIn',
options: {
auth: false, // FIXME: auth: 'guest'
},
data: () => ({
confirm: '',
form: {
username: '',
password: '',
},
error: '',
showError: false,
}),
methods: {
register() {
this.$axios
.post('/user', this.form)
.then(() => this.$router.push('/signin'))
.catch((e) => {
if (e.response) {
this.error = e.response.data.error
this.showError = true
}
})
},
},
head: () => ({
title: 'Sign in',
}),
}
</script>

View File

@ -54,8 +54,10 @@
<div class="text-center">
<p class="text-gray-200 text-sm">
Don't have an account?
<a href="#" class="no-underline text-blue-500 font-bold"
>Create an Account</a
<nuxt-link
to="/register"
class="no-underline text-blue-500 font-bold"
>Create an Account</nuxt-link
>
</p>
</div>
@ -81,9 +83,7 @@ export default {
.loginWith('local', {
data: this.form,
})
.then(function (response) {})
// eslint-disable-next-line handle-callback-err
.catch(function (error) {})
.then(() => this.$router.push('/'))
},
},
head: () => ({