Merge branch 'tailwindcss'

This commit is contained in:
Hubert Van De Walle 2020-07-06 16:09:41 +02:00
commit 3b80ae051d
27 changed files with 1375 additions and 1730 deletions

View File

@ -13,7 +13,7 @@ server:
jwt: jwt:
auth: auth:
secret: ${JWT_SECRET:-uiqzRNiMYwbObn/Ps5xTasYVeu/63ZuI+1oB98Ez+lY=} # Can be generated with `openssl rand -base64 32` secret: ${JWT_SECRET:-uiqzRNiMYwbObn/Ps5xTasYVeu/63ZuI+1oB98Ez+lY=} # Can be generated with `openssl rand -base64 32`
validity: 1 validity: 24
unit: HOURS unit: HOURS
refresh: refresh:
secret: ${JWT_REFRESH_SECRET=-wWchkx44YGig4Q5Z7b7+E/3ymGEGd6PS7UGedMul3bg=} # Can be generated with `openssl rand -base64 32` secret: ${JWT_REFRESH_SECRET=-wWchkx44YGig4Q5Z7b7+E/3ymGEGd6PS7UGedMul3bg=} # Can be generated with `openssl rand -base64 32`

18
frontend/Caddyfile Normal file
View File

@ -0,0 +1,18 @@
:7000
log {
format single_field common_log
}
encode gzip
root * dist
file_server
try_files {path} /200.html
@nuxt {
path /_nuxt/*
}
header @nuxt Cache-Control "public,max-age=31536000,immutable" # 1 year

View File

@ -0,0 +1,3 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

View File

@ -1,14 +1,3 @@
/* roboto-regular - latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Roboto'), local('Roboto-Regular'),
url('./fonts/roboto-v20-latin-regular.woff2') format('woff2')
}
.page-enter-active, .page-leave-active { .page-enter-active, .page-leave-active {
transition: opacity .2s; transition: opacity .2s;
} }

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,41 +1,11 @@
<template> <template>
<v-app dark> <div class="h-screen font-sans text-gray-100">
<Navbar /> <nuxt />
<v-main> </div>
<v-container fill-height>
<v-row no-gutters justify="center">
<nuxt />
</v-row>
<v-snackbar v-model="toast" :color="color">{{
msg
}}</v-snackbar>
</v-container>
</v-main>
</v-app>
</template> </template>
<script>
import Navbar from '@/components/Navbar'
export default {
components: { Navbar },
data: () => ({
toast: false,
color: '',
msg: '',
}),
mounted() {
this.$root.$on('toast', (msg, color) => {
this.msg = msg
this.color = color || ''
this.toast = true
})
},
}
</script>
<style> <style>
html { html {
overflow-y: auto; @apply bg-gray-900;
} }
</style> </style>

View File

@ -1,13 +1,3 @@
<template> <template>
<v-app> <nuxt />
<v-main>
<nuxt />
</v-main>
</v-app>
</template> </template>
<style>
html {
overflow-y: auto;
}
</style>

View File

@ -1,15 +1,21 @@
<template> <template>
<v-app dark> <div
<h1 v-if="error.statusCode === 404"> class="h-screen container mx-auto h-full flex justify-center items-center"
{{ pageNotFound }} >
</h1> <div
<h1 v-else> class="text-gray-200 w-full md:w-1/2 lg:w-1/3 bg-gray-800 border-teal-500 p-8 border-t-8 bg-white mb-6 rounded-lg shadow-lg"
{{ otherError }} >
</h1> <h1 v-if="error.statusCode === 404">
<NuxtLink to="/"> {{ pageNotFound }}
Home page </h1>
</NuxtLink> <h1 v-else>
</v-app> {{ otherError }}
</h1>
<NuxtLink to="/" class="text-blue-200 underline">
Home page
</NuxtLink>
</div>
</div>
</template> </template>
<script> <script>

View File

@ -34,14 +34,14 @@ export default ({ command }) => ({
/* /*
** Plugins to load before mounting the App ** Plugins to load before mounting the App
*/ */
plugins: ['~/plugins/theme.client.js'], plugins: [],
/* /*
** Nuxt.js dev-modules ** Nuxt.js dev-modules
*/ */
buildModules: [ buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module // Doc: https://github.com/nuxt-community/eslint-module
'@nuxtjs/vuetify',
'@nuxtjs/eslint-module', '@nuxtjs/eslint-module',
'@nuxtjs/tailwindcss',
], ],
/* /*
** Nuxt.js modules ** Nuxt.js modules
@ -66,7 +66,7 @@ export default ({ command }) => ({
auth: { auth: {
redirect: { redirect: {
login: '/account', login: '/signin',
logout: '/', logout: '/',
home: '/notes', home: '/notes',
}, },
@ -98,45 +98,11 @@ export default ({ command }) => ({
middleware: ['auth'], middleware: ['auth'],
}, },
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
defaultAssets: false,
theme: {
dark: false,
themes: {
dark: {
primary: '#21CFF3',
secondary: '#FF4081',
accent: '#ffe18d',
success: '#4CAF50',
info: '#2196F3',
warning: '#FB8C00',
error: '#FF5252',
},
light: {
primary: '#1976D2',
secondary: '#e91e63',
accent: '#30b1dc',
success: '#4CAF50',
info: '#2196F3',
warning: '#FB8C00',
error: '#FF5252',
},
},
},
},
generate: {
fallback: '404.html',
},
/* /*
** Build configuration ** Build configuration
*/ */
build: { build: {
extractCSS: true, extractCSS: false,
terser: { terser: {
parallel: true, parallel: true,

View File

@ -29,7 +29,7 @@
"@mdi/js": "^5.1.45", "@mdi/js": "^5.1.45",
"@nuxtjs/eslint-config": "^2.0.2", "@nuxtjs/eslint-config": "^2.0.2",
"@nuxtjs/eslint-module": "^1.1.0", "@nuxtjs/eslint-module": "^1.1.0",
"@nuxtjs/vuetify": "^1.0.0", "@nuxtjs/tailwindcss": "^2.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",

View File

@ -1,55 +0,0 @@
<template>
<v-col cols="12" sm="8" md="6" xl="4">
<v-card color="secondary">
<v-btn icon to="/" aria-label="Go back">
<v-icon color="white">{{ mdiArrowLeft }}</v-icon>
</v-btn>
<v-card-title class="text-center justify-center py-6">
<h1 class="font-weight-bold display-2 white--text">
Account
</h1>
</v-card-title>
<v-tabs v-model="tab" grow color="accent">
<v-tab v-for="tab in tabs" :key="tab">
{{ tab }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="tab in tabs" :key="tab">
<v-card flat>
<v-card-text>
<LoginForm v-if="tab === 'Login'"></LoginForm>
<RegisterForm v-else></RegisterForm>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
</v-card>
</v-col>
</template>
<script>
import { mdiArrowLeft } from '@mdi/js'
export default {
options: {
auth: 'guest',
},
data: () => ({
mdiArrowLeft,
tab: 0,
tabs: ['Login', 'Register'],
}),
mounted() {
this.$root.$on('register::success', () => {
this.$root.$emit('toast', 'Welcome', 'success')
this.tab = 0
})
},
head: () => ({
title: 'Account',
}),
}
</script>

View File

@ -1,82 +0,0 @@
<template>
<v-col cols="12">
<v-card color="secondary">
<v-card-title class="text-center justify-center py-6">
<h1 class="font-weight-bold headline white--text">
Create a note
</h1>
<v-spacer />
<v-switch v-model="preview"></v-switch>
</v-card-title>
<v-card flat>
<v-card-text>
<v-textarea
v-if="!preview"
v-model="text"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
label="Text"
auto-grow
></v-textarea>
<client-only v-else>
<!-- html is sanitized -->
<!-- eslint-disable-next-line -->
<div class="html" v-html="html" />
</client-only>
</v-card-text>
</v-card>
</v-card>
</v-col>
</template>
<script>
import markdown from '~/utils/markdown'
export default {
name: 'Create',
data: () => ({
delimiters: [' ', ','],
form: {
title: '',
tags: [],
},
text: '',
possibleTags: [],
conflict: false,
preview: false,
}),
computed: {
html() {
return markdown(this.text)
},
},
mounted() {
this.$axios.get('/tags').then((e) => (this.possibleTags = e.data))
},
methods: {
createNote() {
this.conflict = false
this.$axios
.post(`/notes/${this.form.title}`, {
tags: this.form.tags,
})
.then((_) => {
this.$router.push('/notes')
})
.catch((e) => {
if (e.response && e.response.status === 409) {
this.conflict = true
}
})
},
},
}
</script>
<style>
.html h1 {
margin-bottom: 0.5em;
}
</style>

View File

@ -1,71 +1,50 @@
<template> <template>
<v-col cols="12"> <div>
<v-container fluid> <nav>
<v-row> <ul>
<v-col <li
v-for="(item, index) in items" v-for="link in links"
:key="index" :key="link.name"
class="d-flex" class="text-blue-200"
cols="12"
md="6"
> >
<v-card <nuxt-link :to="link.url" class="underline">{{
:color="item.color" link.name
dark }}</nuxt-link>
style="min-height: 350px; width: 100%;" </li>
> <li class="text-blue-200">
<v-card-title class="headline"> <button class="underline" @click="logout">
{{ item.title }} Sign Out
</v-card-title> </button>
<v-card-subtitle> </li>
{{ item.content }} </ul>
</v-card-subtitle> </nav>
<v-card-actions> <client-only>
<v-btn text>See a demo</v-btn> <div v-if="$auth.$state.loggedIn">
</v-card-actions> User: {{ $auth.$state.user }}
</v-card> </div>
</v-col> </client-only>
</v-row> </div>
</v-container>
</v-col>
</template> </template>
<script> <script>
export default { export default {
title: 'Home', name: 'Home',
data: () => ({
links: [
{ url: '/signin', name: 'Sign In' },
{ url: '/register', name: 'Register' },
{ url: '/notes', name: 'Notes' },
{ url: '/404', name: '404' },
],
}),
options: { options: {
auth: false, auth: false,
}, },
data: () => ({ methods: {
items: [ async logout() {
{ this.$store.commit('notes/clear')
title: 'Writes your notes in markdown', await this.$auth.logout()
content: },
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur aut consectetur debitis harum' + },
' illum in incidunt maxime modi quae velit? Aspernatur, consequatur error' +
' mollitia nesciunt officiis porro ratione sequi voluptas?',
color: 'indigo',
},
{
title: '',
content: '',
color: 'purple',
},
{
title: '',
content: '',
color: 'warning',
},
{
title: '',
content: '',
color: 'teal darken-2',
},
],
}),
head: () => ({
title: 'Home',
}),
} }
</script> </script>
&

View File

@ -1,39 +1,52 @@
<template> <template>
<v-card> <div>
<v-card-title> <div>{{ uuid }}</div>
{{ note.title }} <div v-if="note">{{ note }}</div>
<v-spacer /> <!-- html is sanitized -->
<TagsGroup :tags="note.tags" /> <!-- eslint-disable-next-line -->
</v-card-title> <div class="container mx-auto" id="note" v-html="html"></div>
<v-card-text> </div>
<!-- html is sanitized -->
<!-- eslint-disable-next-line -->
<div v-html="renderedMarkdown"></div>
</v-card-text>
</v-card>
</template> </template>
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'
import renderMarkdown from '@/utils/markdown' import renderMarkdown from '@/utils/markdown'
export default { export default {
name: 'Note', name: 'Note',
data: () => ({ data: () => ({
note: { note: {},
tags: [], html: '',
},
}), }),
computed: { computed: {
renderedMarkdown() { ...mapState('notes', ['notes', 'isInitialized']),
return !this.note.content ...mapGetters('notes', ['find']),
? '' uuid() {
: renderMarkdown(this.note.content).contents return this.$route.params.uuid
}, },
}, },
mounted() { mounted() {
this.$axios if (!this.isInitialized) {
.$get(`/notes/${this.$route.params.uuid}`) this.load().then(() => {
.then((note) => (this.note = note)) this.note = this.find(this.uuid)
this.render()
})
} else {
this.note = this.find(this.uuid)
this.render()
}
},
methods: {
...mapActions('notes', ['load']),
render() {
this.html = renderMarkdown(this.note.content).contents
},
}, },
} }
</script> </script>
<style>
#note h1 {
@apply text-5xl font-bold;
}
</style>

View File

@ -1,46 +1,43 @@
<template> <template>
<div> <div class="container p-4 mx-auto bg-gray-800">
<v-progress-circular <h1 class="text-center">My Notes</h1>
v-if="!isInitialized" <nuxt-link
:size="100" to="/notes/new"
:width="7" class="bg-blue-700 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
color="purple" >New note</nuxt-link
indeterminate >
></v-progress-circular> <input
<v-card v-if="isEmpty"> id="search"
<v-card-text>No notes yet :/</v-card-text> name="search"
</v-card> class="block p-2 my-2 appearance-none w-full bg-transparent border-white-200 border-b focus:border-red-500"
<v-row v-else> placeholder="search"
<v-col aria-label="search"
v-for="note in notes" />
:key="note.uuid" <div v-for="note in notes" :key="note.uuid">
cols="12" <div class="flex justify-between items-center">
md="6" <nuxt-link :to="'/notes/' + note.uuid" class="text-lg">{{
lg="4" note.title
xl="3" }}</nuxt-link>
> <div class="py-2">
<NoteCard <span
:uuid="note.uuid" v-for="(tag, index) in note.tags"
:title="note.title" :key="index"
:updated-at="note.updatedAt" class="inline-block text-sm bg-gray-500 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 ml-2"
:tags="note.tags" >{{ tag }}</span
/> >
</v-col> </div>
</v-row> </div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex' import { mapActions, mapState } from 'vuex'
export default { export default {
name: 'Notes', name: 'Notes',
data: () => ({
loading: true,
}),
computed: { computed: {
...mapState('notes', ['notes', 'isInitialized']), ...mapState('notes', ['notes', 'isInitialized']),
...mapGetters('notes', ['isEmpty']),
}, },
mounted() { mounted() {
if (!this.isInitialized) this.load() if (!this.isInitialized) this.load()
@ -48,8 +45,5 @@ export default {
methods: { methods: {
...mapActions('notes', ['load']), ...mapActions('notes', ['load']),
}, },
head: () => ({
title: 'My notes',
}),
} }
</script> </script>

View File

@ -0,0 +1,44 @@
<template>
<div class="container mx-auto">
<form @submit.prevent="submit">
<textarea
v-model="content"
aria-label="markdown input"
name="content"
rows="30"
style="outline: none !important;"
class="w-full bg-gray-800 p-5"
spellcheck="false"
></textarea>
<button
class="block bg-blue-700 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
type="submit"
>
Save
</button>
</form>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'New',
data: () => ({
title: 'Test',
tags: [],
content: '',
}),
methods: {
...mapActions('notes', ['create']),
submit() {
this.create({
title: this.title,
tags: this.tags,
content: this.content,
})
},
},
}
</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>

93
frontend/pages/signin.vue Normal file
View File

@ -0,0 +1,93 @@
<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">Sign In</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="userLogin">
<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="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">
Don't have an account?
<nuxt-link
to="/register"
class="no-underline text-blue-500 font-bold"
>Create an Account</nuxt-link
>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SignIn',
options: {
auth: false, // FIXME: auth: 'guest'
},
data: () => ({
form: {
username: '',
password: '',
},
}),
methods: {
userLogin() {
this.$auth
.loginWith('local', {
data: this.form,
})
.then(() => this.$router.push('/'))
},
},
head: () => ({
title: 'Sign in',
}),
}
</script>

View File

@ -1,4 +0,0 @@
export default function ({ $vuetify }) {
const theme = localStorage.getItem('theme') ?? 'light'
$vuetify.theme.dark = theme === 'dark'
}

View File

@ -24,9 +24,8 @@ export const mutations = {
} }
export const actions = { export const actions = {
async load({ commit }) { load({ commit }) {
await new Promise((resolve) => setTimeout(resolve, 600)) return this.$axios.get('/notes').then(({ data }) => {
this.$axios.get('/notes').then(({ data }) => {
commit('set', data) commit('set', data)
commit('setInitialized') commit('setInitialized')
}) })
@ -49,7 +48,6 @@ export const actions = {
} }
export const getters = { export const getters = {
isEmpty(state) { isEmpty: (state) => state.isInitialized && state.notes.length === 0,
return state.isInitialized && state.notes.length === 0 find: (state) => (uuid) => state.notes.find((note) => note.uuid === uuid),
},
} }

View File

@ -0,0 +1,38 @@
/*
** TailwindCSS Configuration File
**
** Docs: https://tailwindcss.com/docs/configuration
** Default: https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js
*/
module.exports = {
theme: {
extend: {
colors: {
gray: {
100: '#F8F9FA',
200: '#EBEBEB',
300: '#DEE2E6',
400: '#CED4DA',
500: '#ADB5BD',
600: '#888',
700: '#444',
800: '#303030',
900: '#222',
},
},
},
},
variants: {},
plugins: [],
purge: {
// Learn more on https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css
enabled: process.env.NODE_ENV === 'production',
content: [
'components/**/*.vue',
'layouts/**/*.vue',
'pages/**/*.vue',
'plugins/**/*.js',
'nuxt.config.js',
],
},
}

File diff suppressed because it is too large Load Diff