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:
auth:
secret: ${JWT_SECRET:-uiqzRNiMYwbObn/Ps5xTasYVeu/63ZuI+1oB98Ez+lY=} # Can be generated with `openssl rand -base64 32`
validity: 1
validity: 24
unit: HOURS
refresh:
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 {
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>
<v-app dark>
<Navbar />
<v-main>
<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>
<div class="h-screen font-sans text-gray-100">
<nuxt />
</div>
</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>
html {
overflow-y: auto;
@apply bg-gray-900;
}
</style>

View File

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

View File

@ -1,15 +1,21 @@
<template>
<v-app dark>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
<h1 v-else>
{{ otherError }}
</h1>
<NuxtLink to="/">
Home page
</NuxtLink>
</v-app>
<div
class="h-screen container mx-auto h-full flex justify-center items-center"
>
<div
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"
>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
<h1 v-else>
{{ otherError }}
</h1>
<NuxtLink to="/" class="text-blue-200 underline">
Home page
</NuxtLink>
</div>
</div>
</template>
<script>

View File

@ -34,14 +34,14 @@ export default ({ command }) => ({
/*
** Plugins to load before mounting the App
*/
plugins: ['~/plugins/theme.client.js'],
plugins: [],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module
'@nuxtjs/vuetify',
'@nuxtjs/eslint-module',
'@nuxtjs/tailwindcss',
],
/*
** Nuxt.js modules
@ -66,7 +66,7 @@ export default ({ command }) => ({
auth: {
redirect: {
login: '/account',
login: '/signin',
logout: '/',
home: '/notes',
},
@ -98,45 +98,11 @@ export default ({ command }) => ({
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: {
extractCSS: true,
extractCSS: false,
terser: {
parallel: true,

View File

@ -29,7 +29,7 @@
"@mdi/js": "^5.1.45",
"@nuxtjs/eslint-config": "^2.0.2",
"@nuxtjs/eslint-module": "^1.1.0",
"@nuxtjs/vuetify": "^1.0.0",
"@nuxtjs/tailwindcss": "^2.0.0",
"babel-eslint": "^10.1.0",
"dotenv": "^8.2.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>
<v-col cols="12">
<v-container fluid>
<v-row>
<v-col
v-for="(item, index) in items"
:key="index"
class="d-flex"
cols="12"
md="6"
<div>
<nav>
<ul>
<li
v-for="link in links"
:key="link.name"
class="text-blue-200"
>
<v-card
:color="item.color"
dark
style="min-height: 350px; width: 100%;"
>
<v-card-title class="headline">
{{ item.title }}
</v-card-title>
<v-card-subtitle>
{{ item.content }}
</v-card-subtitle>
<v-card-actions>
<v-btn text>See a demo</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</v-col>
<nuxt-link :to="link.url" class="underline">{{
link.name
}}</nuxt-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 {
title: 'Home',
name: 'Home',
data: () => ({
links: [
{ url: '/signin', name: 'Sign In' },
{ url: '/register', name: 'Register' },
{ url: '/notes', name: 'Notes' },
{ url: '/404', name: '404' },
],
}),
options: {
auth: false,
},
data: () => ({
items: [
{
title: 'Writes your notes in markdown',
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',
}),
methods: {
async logout() {
this.$store.commit('notes/clear')
await this.$auth.logout()
},
},
}
</script>
&

View File

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

View File

@ -1,46 +1,43 @@
<template>
<div>
<v-progress-circular
v-if="!isInitialized"
:size="100"
:width="7"
color="purple"
indeterminate
></v-progress-circular>
<v-card v-if="isEmpty">
<v-card-text>No notes yet :/</v-card-text>
</v-card>
<v-row v-else>
<v-col
v-for="note in notes"
:key="note.uuid"
cols="12"
md="6"
lg="4"
xl="3"
>
<NoteCard
:uuid="note.uuid"
:title="note.title"
:updated-at="note.updatedAt"
:tags="note.tags"
/>
</v-col>
</v-row>
<div class="container p-4 mx-auto bg-gray-800">
<h1 class="text-center">My Notes</h1>
<nuxt-link
to="/notes/new"
class="bg-blue-700 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
>New note</nuxt-link
>
<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">
<nuxt-link :to="'/notes/' + note.uuid" class="text-lg">{{
note.title
}}</nuxt-link>
<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 { mapState, mapGetters, mapActions } from 'vuex'
import { mapActions, mapState } from 'vuex'
export default {
name: 'Notes',
data: () => ({
loading: true,
}),
computed: {
...mapState('notes', ['notes', 'isInitialized']),
...mapGetters('notes', ['isEmpty']),
},
mounted() {
if (!this.isInitialized) this.load()
@ -48,8 +45,5 @@ export default {
methods: {
...mapActions('notes', ['load']),
},
head: () => ({
title: 'My notes',
}),
}
</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 = {
async load({ commit }) {
await new Promise((resolve) => setTimeout(resolve, 600))
this.$axios.get('/notes').then(({ data }) => {
load({ commit }) {
return this.$axios.get('/notes').then(({ data }) => {
commit('set', data)
commit('setInitialized')
})
@ -49,7 +48,6 @@ export const actions = {
}
export const getters = {
isEmpty(state) {
return state.isInitialized && state.notes.length === 0
},
isEmpty: (state) => 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