Merge pull request #4 from levchak0910/add-article-slug-page
feat: add article's page
This commit is contained in:
commit
143b2c3219
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
|
|
@ -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" />
|
||||
|
||||
Follow {{ article.author?.username }}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary">
|
||||
<i class="ion-heart" />
|
||||
|
||||
{{ article.favorited ? "Unfavorite" : "Favorite" }} Article
|
||||
<span class="counter">({{ article.favoritesCount }})</span>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
<RouterLink
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
:to="`/editor/${article.slug}`"
|
||||
>
|
||||
<i class="ion-edit" /> Edit Article
|
||||
</RouterLink>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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') },
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
27
yarn.lock
27
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue