basic ui
This commit is contained in:
1 parent
3bc2a943bd
commit
e3fbe853df
12 files changed
+280
-53
No files matched your search
@@ -1,4 +1,5 @@
|
||||
*.*
|
||||
!*.js
|
||||
!*.ts
|
||||
!*.vue
|
||||
node_modules
|
||||
+2
-5
@@ -3,7 +3,7 @@
|
||||
"tabs": [
|
||||
{
|
||||
"name": "千字文",
|
||||
"to": "/posts/千字文"
|
||||
"to": "posts/2019/08/千字文"
|
||||
},
|
||||
{
|
||||
"name": "Resources",
|
||||
@@ -14,8 +14,5 @@
|
||||
"url": "https://github.com/patarapolw/zhdiary"
|
||||
}
|
||||
],
|
||||
"fullUrl": "https://zhdiary.polvcode.dev",
|
||||
"author": {
|
||||
"name": "Pacharapol Withayasakpunt"
|
||||
}
|
||||
"fullUrl": "https://zhdiary.polvcode.dev"
|
||||
}
|
||||
@@ -47,6 +47,8 @@ const db = new DataStore('assets/data.nedb')
|
||||
...data,
|
||||
date: formatDate(data.date),
|
||||
filename: f,
|
||||
type,
|
||||
slug: f.replace(/^slides\//, '').replace(/\.md$/, ''),
|
||||
h,
|
||||
excerpt: computedExcerpt,
|
||||
slides
|
||||
@@ -66,14 +68,19 @@ const db = new DataStore('assets/data.nedb')
|
||||
computedContent = computedContent.substr(computedExcerpt.length)
|
||||
}
|
||||
|
||||
if (data.date) {
|
||||
console.log(formatDate(data.date))
|
||||
}
|
||||
|
||||
await db.insert({
|
||||
...data,
|
||||
date: formatDate(data.date),
|
||||
filename: f,
|
||||
type: f.split('/')[0],
|
||||
slug: f.startsWith('posts/blog/')
|
||||
? f.replace(/^posts\/blog\//, '').replace(/\.md$/, '')
|
||||
: f.startsWith('posts/')
|
||||
? (() => {
|
||||
const ff = f.split('/')
|
||||
return ff[ff.length - 1].replace(/\.md$/, '')
|
||||
})()
|
||||
: f.replace(/^[^/]+?\//, '').replace(/\.md$/, ''),
|
||||
h,
|
||||
excerpt: computedExcerpt,
|
||||
remaining: computedContent
|
||||
|
||||
@@ -24,30 +24,30 @@ dataRouter.get('/*', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
dataRouter.post('/', async (req, res, next) => {
|
||||
dataRouter.post('/:type', async (req, res, next) => {
|
||||
try {
|
||||
const { q, cond = {}, project, offset, limit } = req.body
|
||||
const $and = [
|
||||
{ filename: { $regex: /^posts\// } },
|
||||
{ type: req.params.type },
|
||||
cond
|
||||
] as any[]
|
||||
]
|
||||
|
||||
if (q) {
|
||||
const parser = new QSearch({
|
||||
dialect: 'nedb',
|
||||
schema: {
|
||||
title: {},
|
||||
filename: {},
|
||||
slug: {},
|
||||
excerpt: {},
|
||||
date: { type: 'date' }
|
||||
}
|
||||
})
|
||||
$and.push(parser.parse(q))
|
||||
$and.push(parser.parse(q).cond)
|
||||
}
|
||||
|
||||
const count = await db.count({ $and })
|
||||
|
||||
let c = db.find({ $and }, project).skip(offset || 0)
|
||||
let c = db.find({ $and }, project).sort({ date: -1 }).skip(offset || 0)
|
||||
|
||||
if (limit !== null) {
|
||||
c = c.limit(limit || 5)
|
||||
|
||||
@@ -9,6 +9,8 @@ export default class MakeHtml {
|
||||
})
|
||||
|
||||
constructor (public id = 'el-' + Math.random().toString(36).substr(2)) {
|
||||
this.md.setFlavor('github')
|
||||
|
||||
this.md.addExtension({
|
||||
type: 'lang',
|
||||
filter (text) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<template lang="pug">
|
||||
.container.m-md
|
||||
h4.title.is-4 Sorry, but no post can be found.
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class Empty extends Vue {}
|
||||
</script>
|
||||
@@ -1,8 +1,9 @@
|
||||
<template lang="pug">
|
||||
.post-meta.mb-sm
|
||||
.post-meta-author
|
||||
a.mr-md(:href="author.link" style="width: 24px; min-width: 24px"): img(:src="author.avatar" style="border-radius: 50%;")
|
||||
a.mr-md(:href="author.link") {{author.login}}
|
||||
.post-meta(style="margin-bottom: 0.5em;")
|
||||
.post-meta-author(v-if="author")
|
||||
a(:href="author.link" style="width: 24px; min-width: 24px")
|
||||
img(:src="author.avatar" style="border-radius: 50%; margin-right: 1em;")
|
||||
a.mr-md(:href="author.link" style="margin-right: 1em;") {{author.login}}
|
||||
div(style="flex-grow: 1")
|
||||
div {{dateString}}
|
||||
</template>
|
||||
@@ -17,10 +18,10 @@ import { config } from '../global'
|
||||
export default class PostHeader extends Vue {
|
||||
@Prop({ required: true }) header!: any
|
||||
|
||||
author = config.author || {}
|
||||
author = config.author
|
||||
|
||||
get dateString () {
|
||||
return this.header.date ? dayjs(this.header.date).format("ddd D MMMM YYYY") : ''
|
||||
return this.header.date ? dayjs(this.header.date).format('ddd D MMMM YYYY') : ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,12 +1,12 @@
|
||||
<template lang="pug">
|
||||
.card.my-lg.blog-post
|
||||
.card.blog-post(style="margin-top: 1em; margin-bottom: 1em")
|
||||
.card-content
|
||||
PostHeader(:header="header")
|
||||
.post-content
|
||||
.image-teaser(v-if="header.image")
|
||||
img(:src="header.image")
|
||||
h2.title {{header.title}}
|
||||
.content(v-html="header.excerpt")
|
||||
.content(v-html="header.excerpt")
|
||||
div(style="display: flex; justify-content: flex-end")
|
||||
router-link.button.is-danger.is-outlined(:to="url") Read more
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@ export default class PostTeaser extends Vue {
|
||||
@Prop({ required: true }) header!: any
|
||||
|
||||
get url () {
|
||||
return this.header.filename.replace(/\.md$/, '')
|
||||
return 'posts/' + this.header.slug
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -36,10 +36,12 @@ export default class PostTeaser extends Vue {
|
||||
margin: 0;
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.image-teaser {
|
||||
width: calc(100% + 3rem);
|
||||
margin-top: 1rem;
|
||||
@@ -47,6 +49,7 @@ export default class PostTeaser extends Vue {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: -1.5rem;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 800px) {
|
||||
.image-teaser {
|
||||
width: 100%;
|
||||
@@ -55,6 +58,7 @@ export default class PostTeaser extends Vue {
|
||||
float: right;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ section
|
||||
:target="t.to ? '' : '_blank'"
|
||||
) {{t.name}}
|
||||
template(slot="end")
|
||||
form.field.m-sm.px-md(@submit.prevent="onSearch")
|
||||
form.field.m-sm.px-md(@submit.prevent="onSearch" style="margin: 0.4rem; padding-left: 0.5rem; padding-right: 0.5rem")
|
||||
.control.has-icons-right
|
||||
input.input.is-rounded(type="search" placeholder="Search" v-model="q")
|
||||
span.icon.is-right
|
||||
@@ -34,20 +34,21 @@ import { normalizeArray } from '../utils'
|
||||
|
||||
@Component<DefaultLayout>({
|
||||
metaInfo () {
|
||||
const url = this.config.fullUrl + this.$route.path;
|
||||
const url = this.config.fullUrl + this.$route.path
|
||||
return {
|
||||
titleTemplate: '%s - ZhDiary',
|
||||
link: [
|
||||
{
|
||||
rel: 'canonical',
|
||||
href: url,
|
||||
},
|
||||
href: url
|
||||
}
|
||||
],
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:url',
|
||||
property: 'og:url',
|
||||
content: url
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -56,16 +57,17 @@ export default class DefaultLayout extends Vue {
|
||||
config = config
|
||||
q = ''
|
||||
|
||||
mounted() {
|
||||
this.q = normalizeArray(this.$route.query.q) || "";
|
||||
mounted () {
|
||||
this.q = normalizeArray(this.$route.query.q) || ''
|
||||
}
|
||||
onSearch() {
|
||||
|
||||
onSearch () {
|
||||
this.$router.push({
|
||||
path: "/",
|
||||
path: '/',
|
||||
query: {
|
||||
q: this.q
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,13 @@ const router = new VueRouter({
|
||||
meta: {
|
||||
layout: 'default'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/posts/*',
|
||||
component: () => import('../views/PostView.vue'),
|
||||
meta: {
|
||||
layout: 'default'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<template lang="pug">
|
||||
section
|
||||
.blog-post(v-if="post.excerpt")
|
||||
.card(style="margin-top: 1rem; margin-bottom: 1rem;")
|
||||
.card-content.content
|
||||
PostHeader(:header="post")
|
||||
h1.title {{post.title}}
|
||||
.image-full(v-if="post.image")
|
||||
img(:src="post.image" :alt="post.title")
|
||||
div(v-html="post.excerpt + post.remaining")
|
||||
div(style="word-break: break-word")
|
||||
span(style="margin-right: 0.5rem;") Tag:
|
||||
span(v-for="t in post.tag || []" :key="t" style="margin-right: 0.5rem;")
|
||||
router-link(:to="'/tag/' + t") {{t}}
|
||||
.card.my-lg
|
||||
.card-content
|
||||
vue-disqus(v-if="disqus" :shortname="disqus" :identifier="$route.path")
|
||||
Empty(v-else)
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
import { api } from '../utils'
|
||||
import PostHeader from '../components/PostHeader.vue'
|
||||
import Empty from '../components/Empty.vue'
|
||||
|
||||
@Component<PostView>({
|
||||
components: {
|
||||
PostHeader,
|
||||
Empty
|
||||
},
|
||||
beforeRouteEnter ({ params }, from, next) {
|
||||
api.post('/data/posts', {
|
||||
cond: {
|
||||
slug: params.pathMatch
|
||||
},
|
||||
limit: 1,
|
||||
project: {
|
||||
title: 1,
|
||||
excerpt: 1,
|
||||
remaining: 1,
|
||||
date: 1,
|
||||
tag: 1
|
||||
}
|
||||
}).then(({ data }) => {
|
||||
next((vm) => {
|
||||
vm.post = data.data[0] || {}
|
||||
})
|
||||
})
|
||||
},
|
||||
beforeRouteUpdate ({ query, params }, from, next) {
|
||||
api.post('/data/posts', {
|
||||
cond: {
|
||||
slug: params.pathMatch
|
||||
},
|
||||
limit: 1,
|
||||
project: {
|
||||
title: 1,
|
||||
excerpt: 1,
|
||||
remaining: 1,
|
||||
date: 1,
|
||||
tag: 1
|
||||
}
|
||||
}).then(({ data }) => {
|
||||
this.$set(this, 'post', data.data[0] || {})
|
||||
next()
|
||||
})
|
||||
},
|
||||
metaInfo () {
|
||||
const title = this.post.title
|
||||
const description = (() => {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = this.post.excerpt
|
||||
return div.innerText
|
||||
})()
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: description },
|
||||
{
|
||||
hid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: this.post.title
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
property: 'og:description',
|
||||
content: description
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
property: 'og:image',
|
||||
content: this.post.image
|
||||
},
|
||||
{
|
||||
hid: 'twitter:title',
|
||||
property: 'twitter:title',
|
||||
content: this.post.title
|
||||
},
|
||||
{
|
||||
hid: 'twitter:description',
|
||||
property: 'twitter:description',
|
||||
content: description
|
||||
},
|
||||
{
|
||||
hid: 'twitter:image',
|
||||
property: 'twitter:image',
|
||||
content: this.post.image
|
||||
},
|
||||
{
|
||||
hid: 'twitter:card',
|
||||
property: 'twitter:card',
|
||||
content: 'summary_large_image'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class PostView extends Vue {
|
||||
post: any = {}
|
||||
disqus = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image-full {
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
|
||||
img {
|
||||
min-width: 500px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.image-full {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: -1.5rem;
|
||||
|
||||
img {
|
||||
min-width: unset;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,13 @@
|
||||
<template lang="pug">
|
||||
section
|
||||
section.m-lg(v-if="tag")
|
||||
section(v-if="tag" style="margin: 1rem;")
|
||||
h2.title.is-2
|
||||
span.mr-sm Tag:
|
||||
span(style="margin-right: 0.5rem;") Tag:
|
||||
span(style="font-weight: 400") {{tag}}
|
||||
section(v-if="posts.length > 0")
|
||||
PostTeaser(v-for="p in posts" :header="p" :key="p.filename")
|
||||
b-pagination.my-lg.mx-sm(:current.sync="page" :total="count" :per-page="5"
|
||||
size="default" rounded)
|
||||
PostTeaser(v-for="p in posts" :header="p" :key="p.slug")
|
||||
b-pagination(:current.sync="page" :total="count" :per-page="5"
|
||||
size="default" rounded style="margin-top: 1rem;")
|
||||
div(v-else)
|
||||
Empty
|
||||
</template>
|
||||
@@ -16,17 +16,16 @@ section
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
|
||||
import PostTeaser from '../components/PostTeaser.vue'
|
||||
import { api, normalizeArray } from '../utils'
|
||||
import Empty from '../components/Empty.vue'
|
||||
import { api } from '../utils'
|
||||
|
||||
@Component<QueryView>({
|
||||
components: {
|
||||
PostTeaser
|
||||
PostTeaser,
|
||||
Empty
|
||||
},
|
||||
beforeRouteEnter (to, from, next) {
|
||||
console.log(to)
|
||||
const { query, params } = to
|
||||
|
||||
api.post('/data/', {
|
||||
beforeRouteEnter ({ query, params }, from, next) {
|
||||
api.post('/data/posts', {
|
||||
q: query.q,
|
||||
cond: params.tag ? {
|
||||
tag: params.tag
|
||||
@@ -35,7 +34,7 @@ import { api, normalizeArray } from '../utils'
|
||||
limit: 5,
|
||||
project: {
|
||||
title: 1,
|
||||
filename: 1,
|
||||
slug: 1,
|
||||
excerpt: 1,
|
||||
date: 1,
|
||||
tag: 1
|
||||
@@ -48,7 +47,7 @@ import { api, normalizeArray } from '../utils'
|
||||
})
|
||||
},
|
||||
beforeRouteUpdate ({ query, params }, from, next) {
|
||||
api.post('/data/', {
|
||||
api.post('/data/posts', {
|
||||
q: query.q,
|
||||
cond: params.tag ? {
|
||||
tag: params.tag
|
||||
@@ -57,7 +56,7 @@ import { api, normalizeArray } from '../utils'
|
||||
limit: 5,
|
||||
project: {
|
||||
title: 1,
|
||||
filename: 1,
|
||||
slug: 1,
|
||||
excerpt: 1,
|
||||
date: 1,
|
||||
tag: 1
|
||||
@@ -67,6 +66,58 @@ import { api, normalizeArray } from '../utils'
|
||||
this.$set(this, 'count', data.count)
|
||||
next()
|
||||
})
|
||||
},
|
||||
metaInfo () {
|
||||
const { tag, page } = this.$route.params
|
||||
const p0 = this.posts[0] || {}
|
||||
const title = (tag ? `Tag: ${tag}` : 'Posts') + (page ? ` - Page ${page}` : '')
|
||||
const description = p0.excerpt ? (() => {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = p0.excerpt
|
||||
return div.innerText
|
||||
})() : ''
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: description },
|
||||
{
|
||||
hid: 'og:title',
|
||||
property: 'og:title',
|
||||
content: p0.title
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
property: 'og:description',
|
||||
content: description
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
property: 'og:image',
|
||||
content: p0.image
|
||||
},
|
||||
{
|
||||
hid: 'twitter:title',
|
||||
property: 'twitter:title',
|
||||
content: p0.title
|
||||
},
|
||||
{
|
||||
hid: 'twitter:description',
|
||||
property: 'twitter:description',
|
||||
content: description
|
||||
},
|
||||
{
|
||||
hid: 'twitter:image',
|
||||
property: 'twitter:image',
|
||||
content: p0.image
|
||||
},
|
||||
{
|
||||
hid: 'twitter:card',
|
||||
property: 'twitter:card',
|
||||
content: 'summary_large_image'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class QueryView extends Vue {
|
||||
@@ -101,9 +152,5 @@ export default class QueryView extends Vue {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get q () {
|
||||
return normalizeArray(this.$route.query.q)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user