Merge pull request #4 from levchak0910/add-article-slug-page

feat: add article's page
This commit is contained in:
mutoe 2020-10-07 11:56:32 +08:00 committed by GitHub
commit 143b2c3219
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 246 additions and 0 deletions

View File

@ -11,11 +11,15 @@
"test": "yarn tsc && yarn lint && yarn test:unit && yarn test:e2e"
},
"dependencies": {
"dompurify": "^2.1.1",
"marked": "^1.2.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0-beta.12"
},
"devDependencies": {
"@types/dompurify": "^2.0.4",
"@types/jest": "^26.0.14",
"@types/marked": "^1.1.0",
"@typescript-eslint/eslint-plugin": "^4.3.0",
"@typescript-eslint/parser": "^4.2.0",
"@vue/compiler-sfc": "^3.0.0-rc.1",

View File

@ -0,0 +1,48 @@
<template>
<div class="card">
<div class="card-block">
<p class="card-text">
{{ comment.body }}
</p>
</div>
<div class="card-footer">
<RouterLink
:to="`/profile/${comment.author.username}`"
class="comment-author"
>
<img
:src="comment.author.image"
class="comment-author-img"
>
</RouterLink>
&nbsp;
<RouterLink
:to="`/profile/${comment.author.username}`"
class="comment-author"
>
{{ comment.author.username }}
</RouterLink>
<span class="date-posted">{{ (new Date(comment.createdAt)).toLocaleDateString() }}</span>
<span class="mod-options">
<i class="ion-edit" />
<i class="ion-trash-a" />
</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'ArticleMeta',
props: {
comment: { type: Object as PropType<ArticleComment>, required: true },
},
})
</script>

View File

@ -0,0 +1,64 @@
<template>
<div class="article-meta">
<a href=""><img :src="article.author?.image"></a>
<div class="info">
<RouterLink
:to="`/profile/${article.author?.username}`"
class="author"
>
{{ article.author?.username }}
</RouterLink>
<span class="date">{{ (new Date(article.createdAt)).toLocaleDateString() }}</span>
</div>
<button class="btn btn-sm btn-outline-secondary">
<i class="ion-plus-round" />
&nbsp;
Follow {{ article.author?.username }}
</button>
&nbsp;&nbsp;
<button class="btn btn-sm btn-outline-primary">
<i class="ion-heart" />
&nbsp;
{{ article.favorited ? "Unfavorite" : "Favorite" }} Article
<span class="counter">({{ article.favoritesCount }})</span>
</button>
&nbsp;&nbsp;
<RouterLink
class="btn btn-outline-secondary btn-sm"
:to="`/editor/${article.slug}`"
>
<i class="ion-edit" /> Edit Article
</RouterLink>
&nbsp;&nbsp;
<button class="btn btn-outline-danger btn-sm">
<i class="ion-trash-a" /> Delete Article
</button>
//
<button
class="btn btn-outline-danger btn-sm disabled"
disabled
>
<i class="ion-trash-a" /> Delete Article
</button>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'ArticleMeta',
props: {
article: { type: Object as PropType<Article>, required: true },
},
})
</script>

102
src/pages/Article.vue Normal file
View File

@ -0,0 +1,102 @@
<template>
<div class="article-page">
<div class="banner">
<div class="container">
<h1>{{ article.title }}</h1>
<ArticleMeta :article="article" />
</div>
</div>
<div class="container page">
<div class="row article-content">
<!-- eslint-disable vue/no-v-html -->
<div
class="col-md-12"
v-html="articleHandledBody"
/>
<!-- eslint-enable vue/no-v-html -->
</div>
<hr>
<div class="article-actions">
<ArticleMeta :article="article" />
</div>
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<form class="card comment-form">
<div class="card-block">
<textarea
class="form-control"
placeholder="Write a comment..."
rows="3"
/>
</div>
<div class="card-footer">
<img
:src="article.author?.image"
class="comment-author-img"
>
<button class="btn btn-sm btn-primary">
Post Comment
</button>
</div>
</form>
<ArticleComment
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
import md2html from 'marked'
import DOMPurify from 'dompurify'
import { getArticle } from '../services/article/getArticle'
import { getCommentsByArticle } from '../services'
import ArticleMeta from '../components/ArticleMeta.vue'
import ArticleComment from '../components/ArticleComment.vue'
export default defineComponent({
name: 'Article',
components: {
ArticleMeta,
ArticleComment,
},
setup () {
const route = useRoute()
const slug = route.params.slug as string
const article = reactive<Article>({} as Article)
const comments = ref<ArticleComment[]>([])
watchEffect(async () => {
const articleData = await getArticle(slug)
Object.assign(article, articleData)
const commentsData = await getCommentsByArticle(slug)
comments.value = commentsData
})
const articleHandledBody = computed(
() => !article.body ? '' : md2html(article.body, { sanitizer: DOMPurify.sanitize }),
)
return {
article,
articleHandledBody,
comments,
}
},
})
</script>

View File

@ -7,6 +7,7 @@ const router = createRouter({
{ path: '/', component: Home },
{ path: '/my-feeds', component: Home },
{ path: '/tag/:tag', component: Home },
{ path: '/article/:slug', component: () => import('./pages/Article.vue') },
],
})

View File

@ -687,6 +687,13 @@
"@types/keygrip" "*"
"@types/node" "*"
"@types/dompurify@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.4.tgz#25fce15f1f4b1bc0df0ad957040cf226416ac2d7"
integrity sha512-y6K7NyXTQvjr8hJNsAFAD8yshCsIJ0d+OYEFzULuIqWyWOKL2hRru1I+rorI5U0K4SLAROTNuSUFXPDTu278YA==
dependencies:
"@types/trusted-types" "*"
"@types/estree@*":
version "0.0.45"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
@ -809,6 +816,11 @@
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
"@types/marked@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.1.0.tgz#53509b5f127e0c05c19176fcf1d743a41e00ff19"
integrity sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw==
"@types/mime@*":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
@ -874,6 +886,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/trusted-types@*":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
"@types/yargs-parser@*":
version "15.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@ -2191,6 +2208,11 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
dompurify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.1.1.tgz#b5aa988676b093a9c836d8b855680a8598af25fe"
integrity sha512-NijiNVkS/OL8mdQL1hUbCD6uty/cgFpmNiuFxrmJ5YPH2cXrPKIewoixoji56rbZ6XBPmtM8GA8/sf9unlSuwg==
dotenv-expand@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
@ -4571,6 +4593,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
marked@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.0.tgz#7221ce2395fa6cf6d722e6f2871a32d3513c85ca"
integrity sha512-tiRxakgbNPBr301ihe/785NntvYyhxlqcL3YaC8CaxJQh7kiaEtrN9B/eK2I2943Yjkh5gw25chYFDQhOMCwMA==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"