refactor: implement script setup suger for components

This commit is contained in:
mutoe 2021-02-26 20:46:54 +08:00
parent 2cfb9a7821
commit c0c983dba0
28 changed files with 709 additions and 1238 deletions

View File

@ -5,24 +5,20 @@
"parser": "@typescript-eslint/parser",
"project": "./tsconfig.json",
"sourceType": "module",
"extraFileExtensions": [
".vue",
".d.ts"
]
"extraFileExtensions": [".vue", ".d.ts"]
},
"extends": [
"standard",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"standard-with-typescript",
"plugin:vue/vue3-recommended"
"plugin:vue/vue3-recommended",
"standard-with-typescript"
],
"rules": {
"no-undef": "off",
"comma-dangle": [
"warn",
"always-multiline"
],
"@typescript-eslint/promise-function-async": "off"
"no-unused-vars": "off",
"comma-dangle": ["warn", "always-multiline"],
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}
}

View File

@ -31,7 +31,7 @@ yarn build
- [x] Unit test ([Vue Test Utils](https://github.com/vuejs/vue-test-utils-next))
- [ ] Unit test ([Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro)) (Temporarily unavailable)
- [x] E2E test ([Cypress](https://docs.cypress.io))
- [ ] [SFC Script Setup](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md) (Unstable)
- [x] [SFC Script Setup](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md) (Experimental)
- [x] Vetur Tools: [VTI](https://github.com/mutoe/vue3-realworld-example-app/pull/28) and [optionally IDE hints](https://github.com/mutoe/vue3-realworld-example-app/commit/8367f89a99c467d181d9c7f4144deb05cec55210#commitcomment-43957089)
# Contributors

View File

@ -1,7 +1,4 @@
{
"baseUrl": "https://mutoe.github.io/vue3-realworld-example-app",
"experimentalNetworkStubbing": true,
"chromeWebSecurity": false
}

View File

@ -1,6 +1,6 @@
{
"name": "vue3-realworld-example-app",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT",
"scripts": {
"dev": "vite",
@ -15,8 +15,8 @@
"@harlem/core": "^1.1.0",
"deepmerge": "^4.2.2",
"dompurify": "^2.2.6",
"marked": "^2.0.0",
"vue": "^3.0.2",
"marked": "^1.2.9",
"vue": "^3.0.6",
"vue-router": "^4.0.4"
},
"devDependencies": {
@ -25,6 +25,7 @@
"@testing-library/vue": "^6.3.4",
"@types/dompurify": "^2.2.1",
"@types/jest": "^26.0.20",
"@types/marked": "^1.2.2",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"@vitejs/plugin-vue": "^1.1.4",
@ -39,14 +40,13 @@
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.6.0",
"husky": "^5.1.1",
"jest": "^26.6.3",
"jsdom": "^16.4.0",
"lint-staged": "^10.5.4",
"ts-jest": "^26.5.1",
"typescript": "^4.1.5",
"typescript": "~4.1.5",
"vite": "^2.0.2",
"vti": "^0.0.24",
"vue-jest": "^5.0.0-alpha.8"

View File

@ -4,16 +4,7 @@
<AppFooter />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import AppFooter from './components/AppFooter.vue'
import AppNavigation from './components/AppNavigation.vue'
export default defineComponent({
name: 'App',
components: {
AppNavigation,
AppFooter,
},
})
</script>

View File

@ -21,11 +21,3 @@
</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'AppFooter',
})
</script>

View File

@ -1,29 +1,19 @@
<template>
<router-link
:to="to"
v-bind="attrs"
>
<router-link :to="props" v-bind="attrs">
<slot />
</router-link>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import type { RouteParams } from 'vue-router'
<script lang="ts" setup>
import type { AppRouteNames } from '../router'
import type { RouteParams } from 'vue-router'
export default defineComponent({
name: 'AppLink',
props: {
name: { type: String as PropType<AppRouteNames>, required: true },
params: { type: Object as PropType<RouteParams>, default: () => ({}) },
},
setup (props, { attrs }) {
return {
to: props,
attrs,
}
},
})
import { defineProps, useContext } from 'vue'
const props = defineProps<{
name: AppRouteNames
params?: RouteParams
}>()
const { attrs } = useContext()
</script>

View File

@ -1,29 +1,17 @@
<template>
<nav class="navbar navbar-light">
<div class="container">
<AppLink
class="navbar-brand"
name="global-feed"
>
conduit
</AppLink>
<AppLink class="navbar-brand" name="global-feed"> conduit </AppLink>
<ul class="nav navbar-nav pull-xs-right">
<li
v-for="link in navLinks"
:key="link.name"
class="nav-item"
>
<li v-for="link in navLinks" :key="link.name" class="nav-item">
<AppLink
class="nav-link"
active-class="active"
:name="link.name"
:params="link.params"
>
<i
v-if="link.icon"
:class="link.icon"
/> {{ link.title }}
<i v-if="link.icon" :class="link.icon" /> {{ link.title }}
</AppLink>
</li>
</ul>
@ -31,12 +19,11 @@
</nav>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
<script lang="ts" setup>
import type { RouteParams } from 'vue-router'
import type { AppRouteNames } from '../router'
import { computed } from 'vue'
import { user } from '../store/user'
interface NavLink {
@ -47,55 +34,46 @@ interface NavLink {
display: 'all' | 'anonym' | 'authorized'
}
export default defineComponent({
name: 'AppNavigation',
setup () {
const username = computed(() => user.value?.username)
const displayStatus = computed(() => username.value ? 'authorized' : 'anonym')
const username = computed(() => user.value?.username)
const displayStatus = computed(() => username.value ? 'authorized' : 'anonym')
const allNavLinks = computed<NavLink[]>(() => [
{
name: 'global-feed',
title: 'Home',
display: 'all',
},
{
name: 'login',
title: 'Sign in',
display: 'anonym',
},
{
name: 'register',
title: 'Sign up',
display: 'anonym',
},
{
name: 'create-article',
title: 'New Post',
display: 'authorized',
icon: 'ion-compose',
},
{
name: 'settings',
title: 'Settings',
display: 'authorized',
icon: 'ion-gear-a',
},
{
name: 'profile',
params: { username: username.value },
title: username.value || '',
display: 'authorized',
},
])
const navLinks = computed(() => allNavLinks.value.filter(
l => l.display === displayStatus.value || l.display === 'all',
))
return {
navLinks,
}
const allNavLinks = computed<NavLink[]>(() => [
{
name: 'global-feed',
title: 'Home',
display: 'all',
},
})
{
name: 'login',
title: 'Sign in',
display: 'anonym',
},
{
name: 'register',
title: 'Sign up',
display: 'anonym',
},
{
name: 'create-article',
title: 'New Post',
display: 'authorized',
icon: 'ion-compose',
},
{
name: 'settings',
title: 'Settings',
display: 'authorized',
icon: 'ion-gear-a',
},
{
name: 'profile',
params: { username: username.value },
title: username.value || '',
display: 'authorized',
},
])
const navLinks = computed(() => allNavLinks.value.filter(
l => l.display === displayStatus.value || l.display === 'all',
))
</script>

View File

@ -13,31 +13,18 @@
</ul>
</template>
<script lang="ts">
import { defineComponent, computed, toRefs } from 'vue'
<script lang="ts" setup>
import { computed, defineEmit, defineProps, toRefs } from 'vue'
import { limit } from '../services'
export default defineComponent({
name: 'AppPagination',
props: {
page: { type: Number, required: true },
count: { type: Number, required: true },
},
emits: {
'page-change': (index: number) => typeof index === 'number',
},
setup (props, { emit }) {
const { count, page } = toRefs(props)
const pagesCount = computed(() => Math.ceil(count.value / limit))
const isActive = (index: number) => page.value === index
const onPageChange = (index: number) => emit('page-change', index)
return {
pagesCount,
isActive,
onPageChange,
}
},
})
const props = defineProps<{
page: number
count: number
}>()
const emit = defineEmit<(e: 'page-change', index: number) => void>()
const { count, page } = toRefs(props)
const pagesCount = computed(() => Math.ceil(count.value / limit))
const isActive = (index: number) => page.value === index
const onPageChange = (index: number) => emit('page-change', index)
</script>

View File

@ -3,20 +3,14 @@
<div class="container">
<h1>{{ article.title }}</h1>
<ArticleDetailMeta
:article="article"
@update="updateArticle"
/>
<ArticleDetailMeta :article="article" @update="updateArticle" />
</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"
/>
<div class="col-md-12" v-html="articleHandledBody" />
<!-- eslint-enable vue/no-v-html -->
<ul class="tag-list">
<li
@ -29,49 +23,29 @@
</ul>
</div>
<hr>
<hr />
<div class="article-actions">
<ArticleDetailMeta
:article="article"
@update="updateArticle"
/>
<ArticleDetailMeta :article="article" @update="updateArticle" />
</div>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import DOMPurify from 'dompurify'
import md2html from 'marked'
import { computed, defineComponent, reactive } from 'vue'
import { computed, reactive } from 'vue'
import { useRoute } from 'vue-router'
import ArticleDetailMeta from './ArticleDetailMeta.vue'
import { getArticle } from '../services/article/getArticle'
export default defineComponent({
name: 'ArticleDetail',
components: {
ArticleDetailMeta,
},
async setup () {
const route = useRoute()
const slug = route.params.slug as string
const article = reactive<Article>(await getArticle(slug))
const route = useRoute()
const slug = route.params.slug as string
const article = reactive<Article>(await getArticle(slug))
const articleHandledBody = computed(() => md2html(article.body, { sanitizer: DOMPurify.sanitize }))
const articleHandledBody = computed(() => md2html(article.body, { sanitizer: DOMPurify.sanitize }))
const updateArticle = (newArticle: Article) => {
Object.assign(article, newArticle)
}
return {
article,
articleHandledBody,
slug,
updateArticle,
}
},
})
const updateArticle = (newArticle: Article) => {
Object.assign(article, newArticle)
}
</script>

View File

@ -41,24 +41,17 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, defineEmit, defineProps } from 'vue'
export default defineComponent({
name: 'ArticleDetailComment',
props: {
comment: { type: Object as PropType<ArticleComment>, required: true },
username: { type: String as PropType<string | undefined>, default: undefined },
},
emits: {
'remove-comment': () => true,
},
setup (props) {
return {
showRemove: computed(() => (
props.username !== undefined && props.username === props.comment.author.username
)),
}
},
})
const props = defineProps<{
comment: ArticleComment
username?: string
}>()
const emit = defineEmit<(e: 'remove-comment') => boolean>()
const showRemove = computed(() => (
props.username !== undefined && props.username === props.comment.author.username
))
</script>

View File

@ -13,8 +13,8 @@
/>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import ArticleDetailCommentsForm from './ArticleDetailCommentsForm.vue'
@ -25,38 +25,22 @@ import { deleteComment } from '../services/comment/postComment'
import { user } from '../store/user'
export default defineComponent({
name: 'ArticleDetailComments',
components: {
ArticleDetailCommentsForm,
ArticleDetailComment,
},
async setup () {
const route = useRoute()
const slug = route.params.slug as string
const route = useRoute()
const slug = route.params.slug as string
const username = computed(() => user.value?.username)
const username = computed(() => user.value?.username)
const comments = ref<ArticleComment[]>([])
const comments = ref<ArticleComment[]>([])
const addComment = async (comment: ArticleComment) => {
comments.value.unshift(comment)
}
const addComment = async (comment: ArticleComment) => {
comments.value.unshift(comment)
}
const removeComment = async (commentId: number) => {
await deleteComment(slug, commentId)
comments.value = comments.value.filter(c => c.id !== commentId)
}
const removeComment = async (commentId: number) => {
await deleteComment(slug, commentId)
comments.value = comments.value.filter(c => c.id !== commentId)
}
comments.value = await getCommentsByArticle(slug)
comments.value = await getCommentsByArticle(slug)
return {
comments,
slug,
username,
addComment,
removeComment,
}
},
})
</script>

View File

@ -35,8 +35,8 @@
</form>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
<script lang="ts" setup>
import { ref, computed, defineProps, defineEmit } from 'vue'
import { useProfile } from '../composable/useProfile'
@ -44,31 +44,21 @@ import { postComment } from '../services/comment/postComment'
import { user, checkAuthorization } from '../store/user'
export default defineComponent({
name: 'ArticleDetailCommentsForm',
props: {
articleSlug: { type: String, required: true },
},
emits: {
'add-comment': (comment: ArticleComment) => !!comment.id,
},
setup (props, { emit }) {
const username = computed(() => checkAuthorization(user) ? user.value.username : '')
const { profile } = useProfile({ username })
const props = defineProps<{
articleSlug: string
}>()
const comment = ref('')
const emit = defineEmit<(e: 'add-comment', comment: ArticleComment) => void>()
const submitComment = async () => {
const newComment = await postComment(props.articleSlug, comment.value)
emit('add-comment', newComment)
comment.value = ''
}
const username = computed(() => checkAuthorization(user) ? user.value.username : '')
const { profile } = useProfile({ username })
const comment = ref('')
const submitComment = async () => {
const newComment = await postComment(props.articleSlug, comment.value)
emit('add-comment', newComment)
comment.value = ''
}
return {
profile,
comment,
submitComment,
}
},
})
</script>

View File

@ -1,22 +1,21 @@
<template>
<div class="article-meta">
<AppLink
name="profile"
:params="{username: article.author.username}"
>
<img :src="article.author.image">
<AppLink name="profile" :params="{ username: article.author.username }">
<img :src="article.author.image" />
</AppLink>
<div class="info">
<AppLink
name="profile"
:params="{username: article.author.username}"
:params="{ username: article.author.username }"
class="author"
>
{{ article.author.username }}
</AppLink>
<span class="date">{{ (new Date(article.createdAt)).toLocaleDateString() }}</span>
<span class="date">{{
new Date(article.createdAt).toLocaleDateString()
}}</span>
</div>
<button
@ -26,17 +25,18 @@
@click="toggleFollow"
>
<i class="ion-plus-round space" />
{{ article.author.following ? "Unfollow" : "Follow" }} {{ article.author.username }}
{{ article.author.following ? "Unfollow" : "Follow" }}
{{ article.author.username }}
</button>
<button
class="btn btn-sm space"
:class="[article.favorited ? 'btn-primary':'btn-outline-primary']"
:class="[article.favorited ? 'btn-primary' : 'btn-outline-primary']"
:disabled="favoriteProcessGoing"
@click="favoriteArticle"
>
<i class="ion-heart space" />
{{ article.favorited ? 'Unfavorite' : 'Favorite' }} Article
{{ article.favorited ? "Unfavorite" : "Favorite" }} Article
<span class="counter">({{ article.favoritesCount }})</span>
</button>
@ -44,7 +44,7 @@
v-if="displayEditButton"
class="btn btn-outline-secondary btn-sm space"
name="edit-article"
:params="{slug: article.slug}"
:params="{ slug: article.slug }"
>
<i class="ion-edit space" /> Edit Article
</AppLink>
@ -59,8 +59,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, toRefs } from 'vue'
<script lang="ts" setup>
import { computed, defineEmit, defineProps, toRefs } from 'vue'
import { deleteArticle } from '../services/article/deleteArticle'
@ -70,48 +70,33 @@ import { useFollow } from '../composable/useFollowProfile'
import { user, checkAuthorization } from '../store/user'
import { routerPush } from '../router'
export default defineComponent({
name: 'ArticleDetailMeta',
props: {
article: { type: Object as PropType<Article>, required: true },
},
emits: {
update: (article: Article) => !!article.slug,
},
setup (props, { emit }) {
const { article } = toRefs(props)
const displayEditButton = computed(() => checkAuthorization(user) && user.value.username === article.value.author.username)
const displayFollowButton = computed(() => user.value?.username !== article.value.author.username)
const props = defineProps<{
article: Article
}>()
const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
isFavorited: computed(() => article.value.favorited),
articleSlug: computed(() => article.value.slug),
onUpdate: newArticle => emit('update', newArticle),
})
const emit = defineEmit<(e: 'update', article: Article) => void>()
const onDelete = async () => {
await deleteArticle(article.value.slug)
await routerPush('global-feed')
}
const { article } = toRefs(props)
const displayEditButton = computed(() => checkAuthorization(user) && user.value.username === article.value.author.username)
const displayFollowButton = computed(() => user.value?.username !== article.value.author.username)
const { followProcessGoing, toggleFollow } = useFollow({
following: computed(() => article.value.author.following),
username: computed(() => article.value.author.username),
onUpdate: (author: Profile) => {
const newArticle = { ...article.value, author }
emit('update', newArticle)
},
})
const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
isFavorited: computed(() => article.value.favorited),
articleSlug: computed(() => article.value.slug),
onUpdate: newArticle => emit('update', newArticle),
})
return {
displayEditButton,
displayFollowButton,
onDelete,
favoriteProcessGoing,
favoriteArticle,
followProcessGoing,
toggleFollow,
}
const onDelete = async () => {
await deleteArticle(article.value.slug)
await routerPush('global-feed')
}
const { followProcessGoing, toggleFollow } = useFollow({
following: computed(() => article.value.author.following),
username: computed(() => article.value.author.username),
onUpdate: (author: Profile) => {
const newArticle = { ...article.value, author }
emit('update', newArticle)
},
})
</script>

View File

@ -1,24 +1,14 @@
<template>
<ArticlesListNavigation
v-bind="$attrs"
:tag="tag"
:username="username"
/>
<ArticlesListNavigation v-bind="$attrs" :tag="tag" :username="username" />
<div
v-if="articlesDownloading"
class="article-preview"
>
<div v-if="articlesDownloading" class="article-preview">
Articles are downloading...
</div>
<div
v-else-if="articles.length === 0"
class="article-preview"
>
<div v-else-if="articles.length === 0" class="article-preview">
No articles are here... yet.
</div>
<template v-else>
<ArticlePreview
<ArticlesListArticlePreview
v-for="(article, index) in articles"
:key="article.slug"
:article="article"
@ -33,43 +23,23 @@
</template>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import ArticlesListNavigation from './ArticlesListNavigation.vue'
import ArticlesListArticlePreview from './ArticlesListArticlePreview.vue'
import AppPagination from './AppPagination.vue'
import { useArticles } from '../composable/useArticles'
export default defineComponent({
name: 'ArticlesList',
components: {
ArticlePreview: ArticlesListArticlePreview,
AppPagination,
ArticlesListNavigation,
},
const {
fetchArticles,
articlesDownloading,
articlesCount,
articles,
updateArticle,
page,
changePage,
tag,
username,
} = useArticles()
async setup () {
const {
fetchArticles, articlesDownloading,
articlesCount, articles, updateArticle,
page, changePage,
tag, username,
} = useArticles()
await fetchArticles()
return {
articlesDownloading,
articles,
articlesCount,
page,
changePage,
updateArticle,
tag,
username,
}
},
})
await fetchArticles()
</script>

View File

@ -3,24 +3,26 @@
<div class="article-meta">
<AppLink
name="profile"
:params="{username: article.author.username}"
:params="{ username: article.author.username }"
>
<img :src="article.author.image">
</AppLink>
<div class="info">
<AppLink
name="profile"
:params="{username: article.author.username}"
:params="{ username: article.author.username }"
class="author"
>
{{ article.author.username }}
</AppLink>
<span class="date">{{ new Date(article.createdAt).toDateString() }}</span>
<span class="date">{{
new Date(article.createdAt).toDateString()
}}</span>
</div>
<button
class="btn btn-sm pull-xs-right"
:class="[article.favorited ? 'btn-primary':'btn-outline-primary']"
:class="[article.favorited ? 'btn-primary' : 'btn-outline-primary']"
:disabled="favoriteProcessGoing"
@click="favoriteArticle"
>
@ -30,7 +32,7 @@
<AppLink
name="article"
:params="{slug: article.slug}"
:params="{ slug: article.slug }"
class="preview-link"
>
<h1>{{ article.title }}</h1>
@ -49,29 +51,19 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
<script lang="ts" setup>
import { computed, defineEmit, defineProps } from 'vue'
import { useFavoriteArticle } from '../composable/useFavoriteArticle'
export default defineComponent({
name: 'ArticlesListArticlePreview',
props: {
article: { type: Object as PropType<Article>, required: true },
},
emits: {
update: (article: Article) => !!article.slug,
},
setup (props, { emit }) {
const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
isFavorited: computed(() => props.article.favorited),
articleSlug: computed(() => props.article.slug),
onUpdate: (newArticle: Article): void => emit('update', newArticle),
})
const props = defineProps<{
article: Article;
}>()
return {
favoriteProcessGoing,
favoriteArticle,
}
},
const emit = defineEmit<(e: 'update', article: Article) => void>()
const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
isFavorited: computed(() => props.article.favorited),
articleSlug: computed(() => props.article.slug),
onUpdate: (newArticle: Article): void => emit('update', newArticle),
})
</script>

View File

@ -22,81 +22,74 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import type { RouteParams } from 'vue-router'
import type { AppRouteNames } from '../router'
import type { ArticlesType } from '../composable/useArticles'
import { computed, defineProps } from 'vue'
import { isAuthorized } from '../store/user'
interface ArticlesListNavLink {
name: ArticlesType
routeName: AppRouteNames
routeParams?: Partial<RouteParams>
title: string
icon?: string
name: ArticlesType;
routeName: AppRouteNames;
routeParams?: Partial<RouteParams>;
title: string;
icon?: string;
}
export default defineComponent({
name: 'ArticlesListNavigation',
props: {
useGlobalFeed: { type: Boolean, default: false },
useMyFeed: { type: Boolean, default: false },
useTagFeed: { type: Boolean, default: false },
useUserFeed: { type: Boolean, default: false },
useUserFavorited: { type: Boolean, default: false },
tag: { type: String, required: true },
username: { type: String, required: true },
const props = defineProps<{
tag: string;
username: string;
useGlobalFeed?: boolean;
useMyFeed?: boolean;
useTagFeed?: boolean;
useUserFeed?: boolean;
useUserFavorited?: boolean;
}>()
const allLinks = computed<ArticlesListNavLink[]>(() => [
{
name: 'global-feed',
routeName: 'global-feed',
title: 'Global Feed',
},
setup (props) {
const allLinks = computed<ArticlesListNavLink[]>(() => [
{
name: 'global-feed',
routeName: 'global-feed',
title: 'Global Feed',
},
{
name: 'my-feed',
routeName: 'my-feed',
title: 'Your Feed',
},
{
name: 'tag-feed',
routeName: 'tag',
routeParams: { tag: props.tag },
title: props.tag,
icon: 'ion-pound',
},
{
name: 'user-feed',
routeName: 'profile',
routeParams: { username: props.username },
title: 'My articles',
},
{
name: 'user-favorites-feed',
routeName: 'profile-favorites',
routeParams: { username: props.username },
title: 'Favorited Articles',
},
])
const show = computed<Record<ArticlesType, boolean>>(() => ({
'global-feed': props.useGlobalFeed,
'my-feed': props.useMyFeed && isAuthorized.value,
'tag-feed': props.useTagFeed && props.tag !== '',
'user-feed': props.useUserFeed && props.username !== '',
'user-favorites-feed': props.useUserFavorited && props.username !== '',
}))
const links = computed<ArticlesListNavLink[]>(() => allLinks.value.filter(link => show.value[link.name]))
return {
links,
}
{
name: 'my-feed',
routeName: 'my-feed',
title: 'Your Feed',
},
})
{
name: 'tag-feed',
routeName: 'tag',
routeParams: { tag: props.tag },
title: props.tag,
icon: 'ion-pound',
},
{
name: 'user-feed',
routeName: 'profile',
routeParams: { username: props.username },
title: 'My articles',
},
{
name: 'user-favorites-feed',
routeName: 'profile-favorites',
routeParams: { username: props.username },
title: 'Favorited Articles',
},
])
const show = computed<Record<ArticlesType, boolean>>(() => ({
'global-feed': props.useGlobalFeed ?? false,
'my-feed': (props.useMyFeed && isAuthorized.value) ?? false,
'tag-feed': (props.useTagFeed && props.tag !== '') ?? false,
'user-feed': (props.useUserFeed && props.username !== '') ?? false,
'user-favorites-feed': (props.useUserFavorited && props.username !== '') ?? false,
}))
const links = computed<ArticlesListNavLink[]>(() =>
allLinks.value.filter((link) => show.value[link.name]),
)
</script>

View File

@ -6,7 +6,7 @@
v-for="tag in tags"
:key="tag"
name="tag"
:params="{tag}"
:params="{ tag }"
class="tag-pill tag-default"
>
{{ tag }}
@ -14,20 +14,10 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { useTags } from '../composable/useTags'
export default defineComponent({
name: 'PopularTags',
async setup () {
const { tags, fetchTags } = useTags()
const { tags, fetchTags } = useTags()
await fetchTags()
return {
tags,
}
},
})
await fetchTags()
</script>

View File

@ -80,6 +80,7 @@ export function useArticles () {
}
export type ArticlesType = 'global-feed' | 'my-feed' | 'tag-feed' | 'user-feed' | 'user-favorites-feed'
export const articlesTypes: ArticlesType[] = ['global-feed', 'my-feed', 'tag-feed', 'user-feed', 'user-favorites-feed']
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export const isArticlesType = (type: any): type is ArticlesType => articlesTypes.includes(type)

View File

@ -28,17 +28,7 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import ArticleDetail from '../components/ArticleDetail.vue'
import ArticleDetailComments from '../components/ArticleDetailComments.vue'
export default defineComponent({
name: 'ArticlePage',
components: {
ArticleDetail,
ArticleDetailComments,
},
})
</script>

View File

@ -65,8 +65,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive, ref } from 'vue'
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getArticle } from '../services/article/getArticle'
import { postArticle, putArticle } from '../services/article/postArticle'
@ -78,60 +78,47 @@ interface FormState {
tagList: string[];
}
export default defineComponent({
name: 'EditArticlePage',
setup () {
const route = useRoute()
const router = useRouter()
const slug = computed<string>(() => route.params.slug as string)
const route = useRoute()
const router = useRouter()
const slug = computed<string>(() => route.params.slug as string)
const form = reactive<FormState>({
title: '',
description: '',
body: '',
tagList: [],
})
const newTag = ref<string>('')
const addTag = () => {
form.tagList.push(newTag.value.trim())
newTag.value = ''
}
const removeTag = (tag: string) => {
form.tagList = form.tagList.filter(t => t !== tag)
}
async function fetchArticle (slug: string) {
const article = await getArticle(slug)
// FIXME: I always feel a little wordy here
form.title = article.title
form.description = article.description
form.body = article.body
form.tagList = article.tagList
}
onMounted(() => {
if (slug.value) fetchArticle(slug.value)
})
const onSubmit = async () => {
let article: Article
if (slug.value) {
article = await putArticle(slug.value, form)
} else {
article = await postArticle(form)
}
return router.push({ name: 'article', params: { slug: article.slug } })
}
return {
form,
onSubmit,
newTag,
addTag,
removeTag,
}
},
const form = reactive<FormState>({
title: '',
description: '',
body: '',
tagList: [],
})
const newTag = ref<string>('')
const addTag = () => {
form.tagList.push(newTag.value.trim())
newTag.value = ''
}
const removeTag = (tag: string) => {
form.tagList = form.tagList.filter(t => t !== tag)
}
async function fetchArticle (slug: string) {
const article = await getArticle(slug)
// FIXME: I always feel a little wordy here
form.title = article.title
form.description = article.description
form.body = article.body
form.tagList = article.tagList
}
onMounted(() => {
if (slug.value) fetchArticle(slug.value)
})
const onSubmit = async () => {
let article: Article
if (slug.value) {
article = await putArticle(slug.value, form)
} else {
article = await postArticle(form)
}
return router.push({ name: 'article', params: { slug: article.slug } })
}
</script>

View File

@ -43,17 +43,7 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import ArticlesList from '../components/ArticlesList.vue'
import PopularTags from '../components/PopularTags.vue'
export default defineComponent({
name: 'HomePage',
components: {
ArticlesList,
PopularTags,
},
})
</script>

View File

@ -3,48 +3,35 @@
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">
Sign in
</h1>
<h1 class="text-xs-center">Sign in</h1>
<p class="text-xs-center">
<AppLink name="register">
Need an account?
</AppLink>
<AppLink name="register"> Need an account? </AppLink>
</p>
<ul class="error-messages">
<li
v-for="(error, field) in errors"
:key="field"
>
{{ field }} {{ error ? error[0] : '' }}
<li v-for="(error, field) in errors" :key="field">
{{ field }} {{ error ? error[0] : "" }}
</li>
</ul>
<form
ref="formRef"
@submit.prevent="login"
>
<fieldset
class="form-group"
aria-required="true"
>
<form ref="formRef" @submit.prevent="login">
<fieldset class="form-group" aria-required="true">
<input
v-model="form.email"
class="form-control form-control-lg"
type="email"
required
placeholder="Email"
>
/>
</fieldset>
<fieldset class=" form-group">
<fieldset class="form-group">
<input
v-model="form.password"
class="form-control form-control-lg"
type="password"
required
placeholder="Password"
>
/>
</fieldset>
<button
class="btn btn-lg btn-primary pull-xs-right"
@ -60,43 +47,31 @@
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
<script lang="ts" setup>
import type { PostLoginForm, PostLoginErrors } from '../services/auth/postLogin'
import { reactive, ref } from 'vue'
import { routerPush } from '../router'
import { postLogin, PostLoginForm, PostLoginErrors } from '../services/auth/postLogin'
import { postLogin } from '../services/auth/postLogin'
import { updateUser } from '../store/user'
export default defineComponent({
name: 'LoginPage',
setup () {
const formRef = ref<HTMLFormElement | null>(null)
const form = reactive<PostLoginForm>({
email: '',
password: '',
})
const errors = ref<PostLoginErrors>({})
const login = async () => {
if (!formRef.value?.checkValidity()) return
const result = await postLogin(form)
if (result.isOk()) {
updateUser(result.value)
await routerPush('global-feed')
} else {
errors.value = await result.value.getErrors()
}
}
return {
formRef,
form,
login,
errors,
}
},
const formRef = ref<HTMLFormElement | null>(null)
const form = reactive<PostLoginForm>({
email: '',
password: '',
})
const errors = ref<PostLoginErrors>({})
const login = async () => {
if (!formRef.value?.checkValidity()) return
const result = await postLogin(form)
if (result.isOk()) {
updateUser(result.value)
await routerPush('global-feed')
} else {
errors.value = await result.value.getErrors()
}
}
</script>

View File

@ -38,7 +38,8 @@
@click="toggleFollow"
>
<i class="ion-plus-round space" />
{{ profile.following ? "Unfollow" : "Follow" }} {{ profile.username }}
{{ profile.following ? "Unfollow" : "Follow" }}
{{ profile.username }}
</button>
</template>
</div>
@ -66,47 +67,27 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import ArticlesList from '../components/ArticlesList.vue'
import { useProfile } from '../composable/useProfile'
import { useFollow } from '../composable/useFollowProfile'
import { user, checkAuthorization } from '../store/user'
export default defineComponent({
name: 'ProfilePage',
components: {
ArticlesList,
},
setup () {
const route = useRoute()
const username = computed<string>(() => route.params.username as string)
const route = useRoute()
const username = computed<string>(() => route.params.username as string)
const { profile, updateProfile } = useProfile({ username })
const { profile, updateProfile } = useProfile({ username })
const { followProcessGoing, toggleFollow } = useFollow({
following: computed<boolean>(() => profile.value?.following ?? false),
username,
onUpdate: (newProfileData: Profile) => updateProfile(newProfileData),
})
const showEdit = computed<boolean>(() => checkAuthorization(user) && user.value.username === username.value)
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
return {
profile,
showEdit,
showFollow,
followProcessGoing,
toggleFollow,
}
},
const { followProcessGoing, toggleFollow } = useFollow({
following: computed<boolean>(() => profile.value?.following ?? false),
username,
onUpdate: (newProfileData: Profile) => updateProfile(newProfileData),
})
const showEdit = computed<boolean>(() => checkAuthorization(user) && user.value.username === username.value)
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
</script>
<style scoped>
@ -114,6 +95,6 @@ export default defineComponent({
margin-right: 4px;
}
.align-left {
text-align: left
text-align: left;
}
</style>

View File

@ -3,28 +3,18 @@
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">
Sign up
</h1>
<h1 class="text-xs-center">Sign up</h1>
<p class="text-xs-center">
<AppLink name="login">
Have an account?
</AppLink>
<AppLink name="login"> Have an account? </AppLink>
</p>
<ul class="error-messages">
<li
v-for="(error, field) in errors"
:key="field"
>
{{ field }} {{ error ? error[0] : '' }}
<li v-for="(error, field) in errors" :key="field">
{{ field }} {{ error ? error[0] : "" }}
</li>
</ul>
<form
ref="formRef"
@submit.prevent="register"
>
<form ref="formRef" @submit.prevent="register">
<fieldset class="form-group">
<input
v-model="form.username"
@ -32,7 +22,7 @@
type="text"
required
placeholder="Your Name"
>
/>
</fieldset>
<fieldset class="form-group">
<input
@ -41,7 +31,7 @@
type="email"
required
placeholder="Email"
>
/>
</fieldset>
<fieldset class="form-group">
<input
@ -51,7 +41,7 @@
:minLength="8"
required
placeholder="Password"
>
/>
</fieldset>
<button
type="submit"
@ -67,44 +57,32 @@
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
<script lang="ts" setup>
import type { PostRegisterForm, PostRegisterErrors } from '../services/auth/postRegister'
import { reactive, ref } from 'vue'
import { routerPush } from '../router'
import { postRegister, PostRegisterForm, PostRegisterErrors } from '../services/auth/postRegister'
import { postRegister } from '../services/auth/postRegister'
import { updateUser } from '../store/user'
export default defineComponent({
name: 'RegisterPage',
setup () {
const formRef = ref<HTMLFormElement | null>(null)
const form = reactive<PostRegisterForm>({
username: '',
email: '',
password: '',
})
const errors = ref<PostRegisterErrors>({})
const register = async () => {
if (!formRef.value?.checkValidity()) return
const result = await postRegister(form)
if (result.isOk()) {
updateUser(result.value)
await routerPush('global-feed')
} else {
errors.value = await result.value.getErrors()
}
}
return {
formRef,
form,
register,
errors,
}
},
const formRef = ref<HTMLFormElement | null>(null)
const form = reactive<PostRegisterForm>({
username: '',
email: '',
password: '',
})
const errors = ref<PostRegisterErrors>({})
const register = async () => {
if (!formRef.value?.checkValidity()) return
const result = await postRegister(form)
if (result.isOk()) {
updateUser(result.value)
await routerPush('global-feed')
} else {
errors.value = await result.value.getErrors()
}
}
</script>

View File

@ -3,9 +3,7 @@
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">
Your Settings
</h1>
<h1 class="text-xs-center">Your Settings</h1>
<form @submit.prevent="onSubmit">
<fieldset>
@ -15,7 +13,7 @@
type="text"
class="form-control"
placeholder="URL of profile picture"
>
/>
</fieldset>
<fieldset class="form-group">
<input
@ -23,7 +21,7 @@
type="text"
class="form-control form-control-lg"
placeholder="Your name"
>
/>
</fieldset>
<fieldset class="form-group">
<textarea
@ -39,7 +37,7 @@
type="email"
class="form-control form-control-lg"
placeholder="Email"
>
/>
</fieldset>
<fieldset class="form-group">
<input
@ -47,7 +45,7 @@
type="password"
class="form-control form-control-lg"
placeholder="New Password"
>
/>
</fieldset>
<button
class="btn btn-lg btn-primary pull-xs-right"
@ -59,12 +57,9 @@
</fieldset>
</form>
<hr>
<hr />
<button
class="btn btn-outline-danger"
@click="onLogout"
>
<button class="btn btn-outline-danger" @click="onLogout">
Or click here to logout.
</button>
</div>
@ -73,54 +68,42 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive } from 'vue'
<script lang="ts" setup>
import type { PutProfileForm } from '../services/profile/putProfile'
import { computed, onMounted, reactive } from 'vue'
import { routerPush } from '../router'
import { putProfile, PutProfileForm } from '../services/profile/putProfile'
import { putProfile } from '../services/profile/putProfile'
import { user, checkAuthorization, updateUser } from '../store/user'
export default defineComponent({
name: 'SettingsPage',
setup () {
const form = reactive<PutProfileForm>({})
const form = reactive<PutProfileForm>({})
const onSubmit = async () => {
const filteredForm = Object.entries(form).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {})
const userData = await putProfile(filteredForm)
updateUser(userData)
await routerPush('profile', { username: userData.username })
}
const onSubmit = async () => {
const filteredForm = Object.entries(form).reduce((a, [k, v]) => (v === null ? a : { ...a, [k]: v }), {})
const userData = await putProfile(filteredForm)
updateUser(userData)
await routerPush('profile', { username: userData.username })
}
const onLogout = async () => {
updateUser(null)
await routerPush('global-feed')
}
const onLogout = async () => {
updateUser(null)
await routerPush('global-feed')
}
onMounted(async () => {
if (!checkAuthorization(user)) return await routerPush('login')
onMounted(async () => {
if (!checkAuthorization(user)) return await routerPush('login')
form.image = user.value.image
form.username = user.value.username
form.bio = user.value.bio
form.email = user.value.email
})
const isButtonDisabled = computed(() => (
form.image === user.value?.image &&
form.username === user.value?.username &&
form.bio === user.value?.bio &&
form.email === user.value?.email &&
!form.password
))
return {
form,
onSubmit,
isButtonDisabled,
onLogout,
}
},
form.image = user.value.image
form.username = user.value.username
form.bio = user.value.bio
form.email = user.value.email
})
const isButtonDisabled = computed(() => (
form.image === user.value?.image &&
form.username === user.value?.username &&
form.bio === user.value?.bio &&
form.email === user.value?.email &&
!form.password
))
</script>

View File

@ -75,6 +75,12 @@ export const router = createRouter({
})
export function routerPush (name: AppRouteNames, params?: RouteParams): ReturnType<typeof router.push> {
if (params !== undefined) return router.push({ name, params })
else return router.push({ name })
if (params !== undefined) {
return router.push({
name,
params,
})
} else {
return router.push({ name })
}
}

762
yarn.lock

File diff suppressed because it is too large Load Diff