chore: update eslint rules

This commit is contained in:
mutoe 2023-12-11 18:34:08 +08:00
parent d604eaa4ef
commit ac3c831e2b
No known key found for this signature in database
61 changed files with 942 additions and 869 deletions

View File

@ -1,10 +0,0 @@
module.exports = {
root: true,
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
extraFileExtensions: ['.vue'],
},
extends: '@mutoe/eslint-config-preset-vue',
}

View File

@ -6,13 +6,13 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
directory: /
schedule:
interval: monthly
allow:
- dependency-type: production
- package-ecosystem: github-actions
directory: ".github/workflows"
directory: .github/workflows
schedule:
interval: monthly

View File

@ -9,7 +9,7 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: CodeQL
on:
push:
@ -28,7 +28,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
language: [javascript]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed

View File

@ -25,7 +25,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install

View File

@ -25,7 +25,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@ -55,7 +55,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@ -102,7 +102,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install --no-frozen-lockfile

View File

@ -25,7 +25,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install
@ -55,7 +55,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install
@ -102,7 +102,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
cache: pnpm
- name: Install dependencies
run: pnpm install

View File

@ -34,7 +34,6 @@ Instead of having the Bootstrap theme included locally, we recommend loading the
Alternatively, if you want to make modifications to the theme, check out the [theme's repo](https://github.com/gothinkster/conduit-bootstrap-template).
# Templates
- [Layout](#layout)
@ -48,10 +47,8 @@ Alternatively, if you want to make modifications to the theme, check out the [th
- [Create/Edit Article](#createedit-article)
- [Article](#article)
## Layout
### Header
```html
@ -93,7 +90,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
</div>
</nav>
```
### Footer
@ -319,7 +315,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
</a>
</div>
</div>
</div>
@ -400,7 +395,6 @@ Alternatively, if you want to make modifications to the theme, check out the [th
</div>
</div>
```
### Article

View File

@ -9,7 +9,6 @@
- [Demo](https://vue3-realworld-example-app-mutoe.vercel.app)
- [RealWorld](https://github.com/gothinkster/realworld)
This codebase was created to demonstrate a fully fledged fullstack application built with **Vue3** including CRUD operations, authentication, routing, pagination, and more.
We've gone to great lengths to adhere to the **Vue3** community styleguides & best practices.

View File

@ -1,20 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: [
'@mutoe/eslint-config-preset-ts',
'plugin:cypress/recommended',
],
env: {
'cypress/globals': true,
},
overrides: [
{
files: ['support/**/*.ts'],
rules: {
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
},
},
],
}

View File

@ -1,6 +1,6 @@
import { ROUTES } from './constant'
describe('Article', () => {
describe('article', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
cy.intercept('GET', /articles\/.+/, { fixture: 'article.json' })

View File

@ -1,13 +1,13 @@
import { ROUTES } from './constant'
describe('Auth', () => {
describe('auth', () => {
beforeEach(() => {
cy.intercept('GET', /users/, { fixture: 'user.json' }).as('getUser')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
cy.intercept('GET', /articles/, { fixture: 'articles.json' }).as('getArticles')
})
describe('Login and logout', () => {
describe('login and logout', () => {
it('should login success when submit a valid login form', () => {
cy.login()
@ -75,7 +75,7 @@ describe('Auth', () => {
})
})
describe('Register', () => {
describe('register', () => {
it('should call register API and jump to home page when submit a valid form', () => {
cy.intercept('POST', /users$/, { fixture: 'user.json' }).as('registerRequest')
cy.visit(ROUTES.REGISTER)

View File

@ -1,6 +1,6 @@
import { ROUTES } from './constant'
describe('Favorite', () => {
describe('favorite', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')

View File

@ -1,11 +1,11 @@
import { ROUTES } from './constant'
describe.only('Follow', () => {
describe.only('follow', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?/, { fixture: 'articles.json' }).as('getArticles')
cy.intercept('GET', /tags/, { fixture: 'tags.json' }).as('getTags')
cy.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' }).as('getProfile')
cy.fixture('article.json').then(article => {
cy.fixture('article.json').then((article) => {
article.article.author.username = 'foo'
cy.intercept('GET', /articles\/\S+/, { statusCode: 200, body: article }).as('getArticle')
})
@ -21,7 +21,7 @@ describe.only('Follow', () => {
})
it('should call follow user api when click follow user button', () => {
cy.fixture('profile.json').then(profile => {
cy.fixture('profile.json').then((profile) => {
profile.profile.following = true
cy.intercept('POST', /profiles\/\S+\/follow/, { statusCode: 200, body: profile }).as('followUser')
})

View File

@ -1,6 +1,6 @@
import { ROUTES } from './constant'
describe('Homepage', () => {
describe('homepage', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')

View File

@ -1,6 +1,6 @@
import { ROUTES } from './constant'
describe('Tag', () => {
describe('tag', () => {
beforeEach(() => {
cy.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' }).as('getArticlesOfTag')
cy.intercept('GET', /articles\?limit/, { fixture: 'articles.json' }).as('getArticles')
@ -31,7 +31,7 @@ describe('Tag', () => {
.should('have.class', 'router-link-active')
.should('have.class', 'router-link-exact-active')
cy.get('a.tag-pill.tag-default:last').invoke('text').then(tag => {
cy.get('a.tag-pill.tag-default:last').invoke('text').then((tag) => {
const path = `#/tag/${tag}`
cy.url()

View File

@ -1,17 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"baseUrl": ".",
"types": ["cypress", "@testing-library/cypress"],
"isolatedModules": false
},
"include": [
"./**/*",
"../fixtures/**/*",
"../support/commands.ts",
"../support/e2e.ts"
],
"exclude": ["../../src"],
"compilerOptions": {
"baseUrl": ".",
"isolatedModules": false,
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"types": ["cypress", "@testing-library/cypress"]
}
"exclude": [
"../../src"
]
}

View File

@ -1,3 +1,5 @@
/* eslint-disable ts/no-unsafe-member-access,ts/no-unsafe-assignment */
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
@ -27,7 +29,7 @@ declare global {
}
Cypress.Commands.add('login', (username = 'plumrx') => {
cy.fixture('user.json').then(authResponse => {
cy.fixture('user.json').then((authResponse) => {
authResponse.user.username = username
cy.intercept('POST', /users\/login$/, { statusCode: 200, body: authResponse })
})

24
eslint.config.js Normal file
View File

@ -0,0 +1,24 @@
import defineConfig from '@mutoe/eslint-config'
export default defineConfig({
typescript: {
tsconfigPath: [
'tsconfig.json',
'tsconfig.node.json',
'cypress/e2e/tsconfig.json',
],
},
vue: {
sfcBlocks: {
defaultLanguage: {
script: 'ts',
},
},
},
test: {
cypress: true,
},
ignores: [
'src/services/api.ts',
],
})

View File

@ -2,22 +2,29 @@
"name": "vue3-realworld-example-app",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"prepare": "simple-git-hooks",
"dev": "vite",
"build": "vite build",
"serve": "vite preview --port 4137",
"lint:script": "eslint \"{src/**/*.{ts,vue},cypress/**/*.ts}\"",
"lint:tsc": "vue-tsc --noEmit",
"lint": "concurrently \"npm run lint:tsc\" \"npm run lint:script\"",
"test:unit": "vitest run",
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
"type-check": "vue-tsc --noEmit",
"lint": "eslint --fix .",
"test": "npm run test:unit && npm run test:e2e:ci",
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4137\"",
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4137\"",
"test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
"test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
"test:unit": "vitest run",
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts"
},
"simple-git-hooks": {
"pre-commit": "npm exec lint-staged",
"pre-push": "npm run lint && npm run build"
},
"lint-staged": {
"*": "eslint --fix"
},
"dependencies": {
"insane": "^2.6.2",
"marked": "^11.0.0",
@ -26,7 +33,8 @@
"vue-router": "^4.2.5"
},
"devDependencies": {
"@mutoe/eslint-config-preset-vue": "~3.5.3",
"@eslint/eslintrc": "^2.1.4",
"@mutoe/eslint-config": "^2.4.5",
"@pinia/testing": "^0.1.3",
"@testing-library/cypress": "^10.0.1",
"@testing-library/user-event": "^14.5.1",
@ -48,13 +56,5 @@
"vitest": "^1.0.0-beta.5",
"vitest-dom": "^0.1.1",
"vue-tsc": "^1.8.22"
},
"lint-staged": {
"src/**/*.{ts,vue,js}": "eslint --fix",
"cypress/**/*.{ts,js}": "eslint --fix"
},
"simple-git-hooks": {
"pre-commit": "npm exec lint-staged",
"pre-push": "npm run lint && npm run build"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -92,5 +92,4 @@ const allNavLinks = computed<NavLink[]>(() => [
const navLinks = computed(() => allNavLinks.value.filter(
l => l.display === displayStatus.value || l.display === 'all',
))
</script>

View File

@ -3,7 +3,8 @@
<li
v-for="pageNumber in pagesCount"
:key="pageNumber"
:class="['page-item', { active: isActive(pageNumber) }]"
class="page-item"
:class="[{ active: isActive(pageNumber) }]"
>
<a
:aria-label="`Go to page ${pageNumber}`"
@ -24,15 +25,14 @@ interface Props {
count: number
}
interface Emits {
(e: 'page-change', index: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const emit = defineEmits<{
(e: 'pageChange', 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)
const onPageChange = (index: number) => emit('pageChange', index)
</script>

View File

@ -55,7 +55,7 @@ const article: Article = reactive(await api.articles.getArticle(slug).then(res =
const articleHandledBody = computed(() => marked(article.body))
const updateArticle = (newArticle: Article) => {
function updateArticle(newArticle: Article) {
Object.assign(article, newArticle)
}
</script>

View File

@ -26,7 +26,7 @@ describe('# ArticleDetailComment', () => {
expect(getByRole('button', { name: 'Delete comment' })).toBeInTheDocument()
})
it('should emit remove comment when click remove comment button', () => {
it('should emit remove comment when click remove comment button', async () => {
const onRemoveComment = vi.fn()
const { getByRole } = render(ArticleDetailComment, renderOptions({
props: {
@ -36,7 +36,7 @@ describe('# ArticleDetailComment', () => {
},
}))
fireEvent.click(getByRole('button', { name: 'Delete comment' }))
await fireEvent.click(getByRole('button', { name: 'Delete comment' }))
expect(onRemoveComment).toHaveBeenCalled()
})

View File

@ -54,12 +54,11 @@ interface Props {
comment: Comment
username?: string
}
const props = defineProps<Props>()
interface Emits {
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'remove-comment'): boolean
}
const emit = defineEmits<Emits>()
}>()
const showRemove = computed(() => props.username !== undefined && props.username === props.comment.author.username)
</script>

View File

@ -32,15 +32,14 @@ const username = computed(() => user.value?.username)
const comments = ref<Comment[]>([])
const addComment = async (comment: Comment) => {
async function addComment(comment: Comment) {
comments.value.unshift(comment)
}
const removeComment = async (commentId: number) => {
async function removeComment(commentId: number) {
await api.articles.deleteArticleComment(slug, commentId)
comments.value = comments.value.filter(c => c.id !== commentId)
}
comments.value = await api.articles.getArticleComments(slug).then(res => res.data.comments)
</script>

View File

@ -49,12 +49,11 @@ import { useUserStore } from 'src/store/user'
interface Props {
articleSlug: string
}
interface Emits {
(e: 'add-comment', comment: Comment): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const emit = defineEmits<{
(e: 'addComment', comment: Comment): void
}>()
const { user } = storeToRefs(useUserStore())
@ -63,12 +62,11 @@ const { profile } = useProfile({ username })
const comment = ref('')
const submitComment = async () => {
async function submitComment() {
const newComment = await api.articles
.createArticleComment(props.articleSlug, { comment: { body: comment.value } })
.then(res => res.data.comment)
emit('add-comment', newComment)
emit('addComment', newComment)
comment.value = ''
}
</script>

View File

@ -94,7 +94,7 @@ const { favoriteProcessGoing, favoriteArticle } = useFavoriteArticle({
onUpdate: newArticle => emit('update', newArticle),
})
const onDelete = async () => {
async function onDelete() {
await api.articles.deleteArticle(article.value.slug)
await routerPush('global-feed')
}

View File

@ -52,5 +52,4 @@ const {
} = useArticles()
await fetchArticles()
</script>

View File

@ -73,5 +73,4 @@ const {
articleSlug: computed(() => props.article.slug),
onUpdate: (newArticle: Article): void => emit('update', newArticle),
})
</script>

View File

@ -99,5 +99,4 @@ const show = computed<Record<ArticlesType, boolean>>(() => ({
}))
const links = computed<ArticlesListNavLink[]>(() => allLinks.value.filter(link => show.value[link.name]))
</script>

View File

@ -20,16 +20,20 @@ export function useArticles () {
if (articlesType.value === 'my-feed') {
responsePromise = api.articles.getArticlesFeed(pageToOffset(page.value))
.then(res => res.data)
} else if (articlesType.value === 'tag-feed' && tag.value) {
}
else if (articlesType.value === 'tag-feed' && tag.value) {
responsePromise = api.articles.getArticles({ tag: tag.value, ...pageToOffset(page.value) })
.then(res => res.data)
} else if (articlesType.value === 'user-feed' && username.value) {
}
else if (articlesType.value === 'user-feed' && username.value) {
responsePromise = api.articles.getArticles({ author: username.value, ...pageToOffset(page.value) })
.then(res => res.data)
} else if (articlesType.value === 'user-favorites-feed' && username.value) {
}
else if (articlesType.value === 'user-favorites-feed' && username.value) {
responsePromise = api.articles.getArticles({ favorited: username.value, ...pageToOffset(page.value) })
.then(res => res.data)
} else if (articlesType.value === 'global-feed') {
}
else if (articlesType.value === 'global-feed') {
responsePromise = api.articles.getArticles(pageToOffset(page.value))
.then(res => res.data)
}
@ -55,11 +59,10 @@ export function useArticles () {
const { active: articlesDownloading, run: runWrappedFetchArticles } = useAsync(fetchArticles)
watch(metaChanged, async () => {
if (page.value === 1) {
if (page.value === 1)
await runWrappedFetchArticles()
} else {
else
changePage(1)
}
})
watch(page, runWrappedFetchArticles)
@ -81,7 +84,7 @@ export type ArticlesType = 'global-feed' | 'my-feed' | 'tag-feed' | 'user-feed'
export const articlesTypes: ArticlesType[] = ['global-feed', 'my-feed', 'tag-feed', 'user-feed', 'user-favorites-feed']
export const isArticlesType = (type: any): type is ArticlesType => articlesTypes.includes(type)
export const isArticlesType = (type: unknown): type is ArticlesType => articlesTypes.includes(type as ArticlesType)
const routeNameToArticlesType: Partial<Record<AppRouteNames, ArticlesType>> = {
'global-feed': 'global-feed',
@ -106,9 +109,10 @@ function useArticlesMeta (): UseArticlesMetaReturn {
watch(
() => route.name,
routeName => {
(routeName) => {
const possibleArticlesType = routeNameToArticlesType[routeName as AppRouteNames]
if (!isArticlesType(possibleArticlesType)) return
if (!isArticlesType(possibleArticlesType))
return
articlesType.value = possibleArticlesType
},
@ -117,20 +121,18 @@ function useArticlesMeta (): UseArticlesMetaReturn {
watch(
() => route.params.username,
usernameParam => {
if (usernameParam !== username.value) {
(usernameParam) => {
if (usernameParam !== username.value)
username.value = typeof usernameParam === 'string' ? usernameParam : ''
}
},
{ immediate: true },
)
watch(
() => route.params.tag,
tagParam => {
if (tagParam !== tag.value) {
(tagParam) => {
if (tagParam !== tag.value)
tag.value = typeof tagParam === 'string' ? tagParam : ''
}
},
{ immediate: true },
)

View File

@ -9,7 +9,7 @@ interface useFavoriteArticleProps {
onUpdate: (newArticle: Article) => void
}
export const useFavoriteArticle = ({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) => {
export function useFavoriteArticle({ isFavorited, articleSlug, onUpdate }: useFavoriteArticleProps) {
const favoriteArticle = async () => {
const requestor = isFavorited.value ? api.articles.deleteArticleFavorite : api.articles.createArticleFavorite
const article = await requestor(articleSlug.value).then(res => res.data.article)

View File

@ -12,7 +12,9 @@ export function useProfile ({ username }: UseProfileProps) {
async function fetchProfile(): Promise<void> {
updateProfile(null)
if (!username.value) return
if (!username.value)
return
const profileData = await api.profiles.getProfileByUsername(username.value).then(res => res.data.profile)
updateProfile(profileData)
}

View File

@ -1,3 +1,3 @@
export const CONFIG = {
API_HOST: import.meta.env.VITE_API_HOST || '',
API_HOST: String(import.meta.env.VITE_API_HOST) || '',
}

View File

@ -98,11 +98,11 @@ const form: FormState = reactive({
})
const newTag = ref<string>('')
const addTag = () => {
function addTag() {
form.tagList.push(newTag.value.trim())
newTag.value = ''
}
const removeTag = (tag: string) => {
function removeTag(tag: string) {
form.tagList = form.tagList.filter(t => t !== tag)
}
@ -116,18 +116,18 @@ async function fetchArticle (slug: string) {
form.tagList = article.tagList
}
onMounted(() => {
if (slug.value) fetchArticle(slug.value)
onMounted(async () => {
if (slug.value)
await fetchArticle(slug.value)
})
const onSubmit = async () => {
async function onSubmit() {
let article: Article
if (slug.value) {
if (slug.value)
article = await api.articles.updateArticle(slug.value, { article: form }).then(res => res.data.article)
} else {
else
article = await api.articles.createArticle({ article: form }).then(res => res.data.article)
}
return router.push({ name: 'article', params: { slug: article.slug } })
}
</script>

View File

@ -51,7 +51,7 @@ describe('# Login page', () => {
it('should not trigger api call when user submit a invalid form', async () => {
const { getByRole, getByPlaceholderText } = render(Login, renderOptions())
const formElement = getByRole('form', { name: 'Login form' }) as HTMLFormElement
const formElement = getByRole<HTMLFormElement>('form', { name: 'Login form' })
vi.spyOn(formElement, 'checkValidity')
expect(getByRole('button', { name: 'Sign in' })).toHaveProperty('disabled', true)

View File

@ -80,16 +80,18 @@ const { updateUser } = useUserStore()
const errors = ref()
const login = async () => {
async function login() {
errors.value = {}
if (!formRef.value?.checkValidity()) return
if (!formRef.value?.checkValidity())
return
try {
const result = await api.users.login({ user: form })
updateUser(result.data.user)
await routerPush('global-feed')
} catch (error) {
}
catch (error) {
if (isFetchError(error)) {
errors.value = error.error?.errors
return
@ -97,5 +99,4 @@ const login = async () => {
console.error(error)
}
}
</script>

View File

@ -91,7 +91,6 @@ const { user, isAuthorized } = storeToRefs(useUserStore())
const showEdit = computed<boolean>(() => isAuthorized && user.value?.username === username.value)
const showFollow = computed<boolean>(() => user.value?.username !== username.value)
</script>
<style scoped>

View File

@ -56,7 +56,7 @@ describe('# Register form', () => {
it('should not trigger api call when user submit a invalid form', async () => {
const { getByRole, getByPlaceholderText } = render(Register, renderOptions())
const formElement = getByRole('form', { name: 'Registration form' }) as HTMLFormElement
const formElement = getByRole<HTMLFormElement>('form', { name: 'Registration form' })
vi.spyOn(formElement, 'checkValidity')
expect(getByRole('button', { name: 'Sign up' })).toHaveProperty('disabled', true)

View File

@ -89,19 +89,20 @@ const { updateUser } = useUserStore()
const errors = ref()
const register = async () => {
async function register() {
errors.value = {}
if (!formRef.value?.checkValidity()) return
if (!formRef.value?.checkValidity())
return
try {
const result = await api.users.createUser({ user: form })
updateUser(result.data.user)
await routerPush('global-feed')
} catch (error) {
if (isFetchError(error)) {
}
catch (error) {
if (isFetchError(error))
errors.value = error.error?.errors
}
}
}
</script>

View File

@ -97,29 +97,30 @@ const form: UpdateUser = reactive({})
const userStore = useUserStore()
const errors = ref()
const onSubmit = async () => {
async function onSubmit() {
errors.value = {}
try {
// eslint-disable-next-line unicorn/no-array-reduce
// eslint-disable-next-line unicorn/no-array-reduce, ts/no-unsafe-assignment
const filteredForm = Object.entries(form).reduce((form, [k, v]) => v === null ? form : Object.assign(form, { [k]: v }), {})
const userData = await api.user.updateCurrentUser({ user: filteredForm }).then(res => res.data.user)
userStore.updateUser(userData)
await routerPush('profile', { username: userData.username })
} catch (error) {
if (isFetchError(error)) {
}
catch (error) {
if (isFetchError(error))
errors.value = error.error?.errors
}
}
}
const onLogout = async () => {
async function onLogout() {
userStore.updateUser(null)
await routerPush('global-feed')
}
onMounted(async () => {
if (!userStore.isAuthorized) return await routerPush('login')
if (!userStore.isAuthorized)
return await routerPush('login')
form.image = userStore.user?.image
form.username = userStore.user?.username
@ -134,5 +135,4 @@ const isButtonDisabled = computed(() =>
&& form.email === userStore.user?.email
&& !form.password,
)
</script>

View File

@ -1,11 +1,13 @@
/* eslint-disable array-element-newline */
/* eslint-disable antfu/consistent-list-newline */
import insane from 'insane'
import { marked } from 'marked'
export default (markdown?: string): string => {
if (!markdown) return ''
if (!markdown)
return ''
const html = marked(markdown)
// eslint-disable-next-line ts/no-unsafe-return,ts/no-unsafe-call
return insane(html, {
allowedTags: ['a', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'li', 'main',
@ -30,9 +32,9 @@ export default (markdown?: string): string => {
},
filter: ({ tag, attrs }: { tag: string, attrs: Record<string, string> }) => {
// Display checklist
if (tag === 'input') {
if (tag === 'input')
return attrs.type === 'checkbox' && attrs.disabled === ''
}
return true
},
})

View File

@ -3,5 +3,6 @@ import { userStorage } from 'src/store/user'
export default function (): void {
const token = userStorage.get()?.token
if (token !== undefined) api.setSecurityData(token)
if (token !== undefined)
api.setSecurityData(token)
}

View File

@ -6,7 +6,7 @@ export const limit = 10
export const api = new Api({
baseUrl: `${CONFIG.API_HOST}/api`,
securityWorker: token => token ? { headers: { Authorization: `Bearer ${token}` } } : {},
securityWorker: token => token ? { headers: { Authorization: `Bearer ${String(token)}` } } : {},
baseApiParams: {
headers: {
'content-type': ContentType.Json,

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference types="vitest-dom/extend-expect" />
import 'vitest-dom/extend-expect'

View File

@ -17,7 +17,8 @@ export const useUserStore = defineStore('user', () => {
userStorage.set(userData)
api.setSecurityData(userData.token)
user.value = userData
} else {
}
else {
userStorage.remove()
api.setSecurityData(null)
user.value = null

View File

@ -1,9 +1,6 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
import type AppLink from 'src/components/AppLink.vue'
declare module '@vue/runtime-core' {
// noinspection JSUnusedGlobalSymbols
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@ -1,4 +1,4 @@
export const dateFilter = (dateString: string): string => {
export function dateFilter(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
month: 'long',

View File

@ -12,8 +12,9 @@ export default class Storage<T = unknown> {
get(): T | null {
try {
const value = window[this.storageType].getItem(this.key) ?? ''
return JSON.parse(value)
} catch {
return JSON.parse(value) as T
}
catch {
return null
}
}

View File

@ -10,10 +10,12 @@ import { afterAll, afterEach, beforeAll } from 'vitest'
import AppLink from 'src/components/AppLink.vue'
import { routes } from 'src/router'
export const createTestRouter = (base?: string): Router => createRouter({
export function createTestRouter(base?: string): Router {
return createRouter({
routes,
history: createMemoryHistory(base),
})
}
interface RenderOptionsArgs {
props: Record<string, unknown>
@ -52,17 +54,19 @@ export function renderOptions (args: Partial<RenderOptionsArgs> = {}): RenderOpt
...args.initialState,
},
stubActions: args.stubActions ?? false,
})],
}),
],
components: { AppLink },
},
}
const { initialRoute } = args
if (!initialRoute) return result
if (!initialRoute)
return result
return new Promise((resolve) => {
router.replace(initialRoute).then(() => resolve(result))
void router.replace(initialRoute).then(() => resolve(result))
})
}
@ -74,6 +78,7 @@ export function asyncWrapper (component: ReturnType<typeof defineComponent>, pro
{ id: 'root' },
h(Suspense, null, {
default() {
// eslint-disable-next-line ts/no-unsafe-argument
return h(component, props)
},
fallback: h('div', 'Loading...'),
@ -98,11 +103,13 @@ async function waitForServerRequest (server: SetupServer, method: string, url: s
})
server.events.on('response:mocked', ({ requestId: reqId }) => {
if (reqId === expectedRequestId) resolve(expectedRequest)
if (reqId === expectedRequestId)
resolve(expectedRequest)
})
server.events.on('request:unhandled', ({ request: req, requestId: reqId }) => {
if (reqId === expectedRequestId) reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
if (reqId === expectedRequestId)
reject(new Error(`The ${req.method} ${req.url} request was unhandled.`))
})
})
flush && await flushPromises()
@ -141,14 +148,18 @@ type Listener =
export function setupMockServer(...listeners: Listener[]) {
const parseArgs = (args: Listener): [string, string, number, (object | null)] => {
if (args.length === 4) return args
if (args.length === 4)
return args
if (args.length === 3) {
if (typeof args[1] === 'number') return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
if (typeof args[2] === 'number') return [args[0], args[1], args[2], null] // [method, path, status, null]
if (typeof args[1] === 'number')
return ['all', args[0], args[1], args[2] as object] // ['all', path, 200, object]
if (typeof args[2] === 'number')
return [args[0], args[1], args[2], null] // [method, path, status, null]
return [args[0], args[1], 200, args[2]] // [method, path, 200, object]
}
if (args.length === 2) {
if (typeof args[1] === 'string') return [args[0], args[1], 200, null]
if (typeof args[1] === 'string')
return [args[0], args[1], 200, null]
return ['all', args[0], 200, args[1]]
}
return ['all', args[0], 200, null]

View File

@ -17,14 +17,16 @@ export default function useAsync<T extends (...args: unknown[]) => unknown> (fn:
try {
const result = await fn(...args)
return result as ReturnType<T>
} catch (error) {
}
catch (error) {
if (isFetchError(error) && error.status === 401) {
userStorage.remove()
await routerPush('login')
throw new Error('Unauthorized or token expired')
}
throw error
} finally {
}
finally {
active.value = false
}
}

View File

@ -1,28 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"baseUrl": ".",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }],
"references": [
{ "path": "./tsconfig.node.json" }
],
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"ts-node": {
"compilerOptions": {
"module": "ESNext",

View File

@ -1,14 +1,14 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
},
"include": [
"vite.config.ts",
"cypress.config.ts",
".eslintrc.js"
"eslint.config.js"
]
}

View File

@ -1,9 +1,9 @@
/// <reference types="vitest" />
import { fileURLToPath, URL } from 'node:url'
import { URL, fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import analyzer from 'rollup-plugin-analyzer'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({