feat: working SPA
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"lint": "run-s lint:*",
|
"lint": "run-s lint:*",
|
||||||
"lint:oxlint": "oxlint . --fix",
|
"lint:oxlint": "oxlint . --fix",
|
||||||
"lint:eslint": "eslint . --fix --cache",
|
"lint:eslint": "eslint . --fix --cache",
|
||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../articles
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
+1
-9
@@ -1,11 +1,3 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>You did it!</h1>
|
<RouterView />
|
||||||
<p>
|
|
||||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
|
||||||
documentation
|
|
||||||
</p>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export interface MarkdownAttributes {
|
||||||
|
title?: string
|
||||||
|
draft?: string
|
||||||
|
thumbnail?: string
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownData {
|
||||||
|
attributes: MarkdownAttributes
|
||||||
|
html: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { ArticleList, MarkdownAttributes } from '@/interfaces'
|
||||||
|
import type { MarkdownData } from '@interfaces'
|
||||||
|
|
||||||
|
function completeAttributes(
|
||||||
|
srcAttributes: MarkdownAttributes,
|
||||||
|
pathPrefix: string,
|
||||||
|
): MarkdownAttributes {
|
||||||
|
return {
|
||||||
|
draft: srcAttributes.draft ?? 'false',
|
||||||
|
thumbnail: srcAttributes.thumbnail
|
||||||
|
? pathPrefix + srcAttributes.thumbnail
|
||||||
|
: pathPrefix + '/thumbnail.jpg',
|
||||||
|
title: srcAttributes.title ?? 'Untitled',
|
||||||
|
path: pathPrefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadArticle(year: number, month: number, day: number): MarkdownData | null {
|
||||||
|
const path = `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`
|
||||||
|
console.log(path)
|
||||||
|
try {
|
||||||
|
const data = (await import(
|
||||||
|
`@articles/${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/index.md`
|
||||||
|
)) as MarkdownData
|
||||||
|
return {
|
||||||
|
attributes: completeAttributes(data.attributes, `./articles/${path}`),
|
||||||
|
html: data.html.replaceAll('./', `./articles/${path}/`),
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listArticles(): Promise<MarkdownAttributes[]> {
|
||||||
|
const raw_articles = import.meta.glob('@articles/**/index.md')
|
||||||
|
const articles: MarkdownAttributes[] = await Promise.all(
|
||||||
|
Object.keys(raw_articles).map(async (key) => {
|
||||||
|
const data = (await raw_articles[key]()) as MarkdownData
|
||||||
|
return completeAttributes(data.attributes, key.replace('/index.md', ''))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
articles.sort((article1, article2) => article1.path?.localeCompare(article2.path ?? '') ?? 0)
|
||||||
|
return articles
|
||||||
|
}
|
||||||
+10
-3
@@ -1,8 +1,15 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import ArticleView from '@views/ArticleView.vue'
|
||||||
|
import HomeView from '@views/HomeView.vue'
|
||||||
|
import NotFoundView from '@views/NotFoundView.vue'
|
||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(),
|
||||||
routes: [],
|
routes: [
|
||||||
|
{ path: '/', component: HomeView },
|
||||||
|
{ path: '/articles/:year(\\d{4})/:month(\\d{2})/:day(\\d{2})', component: ArticleView },
|
||||||
|
{ path: '/:pathMatch(.*)', component: NotFoundView },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { MarkdownData } from '@interfaces'
|
||||||
|
import { ref, onBeforeMount } from 'vue'
|
||||||
|
import { loadArticle } from '@lib/articles'
|
||||||
|
import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router'
|
||||||
|
import NotFoundView from './NotFoundView.vue'
|
||||||
|
|
||||||
|
const markdownData = ref<MarkdownData | null>(null)
|
||||||
|
const route = useRoute()
|
||||||
|
const articleDate = ref<Date>(new Date())
|
||||||
|
|
||||||
|
async function loadPage(target: RouteLocation) {
|
||||||
|
articleDate.value = new Date(
|
||||||
|
parseInt(target.params.year),
|
||||||
|
parseInt(target.params.month) - 1,
|
||||||
|
parseInt(target.params.day),
|
||||||
|
)
|
||||||
|
markdownData.value = await loadArticle(
|
||||||
|
articleDate.value.getFullYear(),
|
||||||
|
articleDate.value.getMonth() + 1,
|
||||||
|
articleDate.value.getDate(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => loadPage(route))
|
||||||
|
onBeforeRouteUpdate(loadPage)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="!markdownData">
|
||||||
|
<NotFoundView />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h1>ArticleView - {{ articleDate }}</h1>
|
||||||
|
<div v-html="markdownData.html"></div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onBeforeMount } from 'vue'
|
||||||
|
import { listArticles } from '@/lib/articles'
|
||||||
|
import type { MarkdownAttributes } from '@/interfaces'
|
||||||
|
|
||||||
|
const articles = ref<MarkdownAttributes[]>([])
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
const newArticles = await listArticles()
|
||||||
|
console.log(newArticles)
|
||||||
|
articles.value.splice(0, articles.value.length, ...newArticles)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Home View</h1>
|
||||||
|
<div v-for="(attr, index) in articles" v-bind:key="index">
|
||||||
|
<RouterLink :to="attr.path ?? ''">{{ attr.title }}</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
"include": ["env.d.ts", "vite.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Extra safety for array and object lookups, but may have false positives.
|
// Extra safety for array and object lookups, but may have false positives.
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
|
||||||
|
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
|
||||||
'@articles': fileURLToPath(new URL('./articles', import.meta.url)),
|
'@articles': fileURLToPath(new URL('./articles', import.meta.url)),
|
||||||
|
'@interfaces': fileURLToPath(new URL('./src/interfaces.ts', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assetsInclude: [/\.\/articles\/.*(?!\.md)'/],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
declare module '*.md' {
|
||||||
|
// "unknown" would be more detailed depends on how you structure frontmatter
|
||||||
|
const attributes: Record<string, unknown>
|
||||||
|
|
||||||
|
// When "Mode.TOC" is requested
|
||||||
|
const toc: { level: string; content: string }[]
|
||||||
|
|
||||||
|
// When "Mode.HTML" is requested
|
||||||
|
const html: string
|
||||||
|
|
||||||
|
// When "Mode.RAW" is requested
|
||||||
|
const raw: string
|
||||||
|
|
||||||
|
// When "Mode.React" is requested. VFC could take a generic like React.VFC<{ MyComponent: TypeOfMyComponent }>
|
||||||
|
import React from 'react'
|
||||||
|
const ReactComponent: React.VFC
|
||||||
|
|
||||||
|
// When "Mode.Vue" is requested
|
||||||
|
import { ComponentOptions, Component } from 'vue'
|
||||||
|
const VueComponent: ComponentOptions
|
||||||
|
const VueComponentWith: (components: Record<string, Component>) => ComponentOptions
|
||||||
|
|
||||||
|
// Modify below per your usage
|
||||||
|
export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user