feat: working SPA
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
"lint": "run-s lint:*",
|
||||
"lint:oxlint": "oxlint . --fix",
|
||||
"lint:eslint": "eslint . --fix --cache",
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"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>
|
||||
<h1>You did it!</h1>
|
||||
<p>
|
||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
||||
documentation
|
||||
</p>
|
||||
<RouterView />
|
||||
</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({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: HomeView },
|
||||
{ path: '/articles/:year(\\d{4})/:month(\\d{2})/:day(\\d{2})', component: ArticleView },
|
||||
{ path: '/:pathMatch(.*)', component: NotFoundView },
|
||||
],
|
||||
})
|
||||
|
||||
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",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": ["env.d.ts", "vite.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
// Extra safety for array and object lookups, but may have false positives.
|
||||
|
||||
@@ -11,7 +11,11 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': 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)),
|
||||
'@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