@ansidev-oss/vitepress-theme-ansidev 1.0.12 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/Donation-BBPb8v5p.js +66 -0
- package/dist/client/GoogleAnalytics-34XEM7eO.js +25 -0
- package/dist/client/Sharing-CpagX9oy.js +185 -0
- package/dist/client/Swetrix-GKEyGNrV.js +647 -0
- package/dist/client/components/PostDate.test.d.ts +1 -0
- package/dist/client/composables/index.d.ts +1 -0
- package/dist/client/composables/lang.d.ts +27 -0
- package/dist/client/composables/lang.test.d.ts +1 -0
- package/dist/client/index.js +3769 -9
- package/dist/client/medium-zoom-DPl0T7qn.js +266 -0
- package/dist/client/record-Cgv_fXDH.js +2739 -0
- package/dist/client/styles.css +2 -1
- package/dist/client/types/index.d.ts +8 -0
- package/dist/node/composables.d.mts +16 -17
- package/dist/node/composables.mjs +39 -15
- package/dist/node/config.d.mts +2 -17
- package/dist/node/config.mjs +9 -6
- package/package.json +27 -21
- package/src/client/components/DonationWidget.vue +9 -3
- package/src/client/components/Footer.vue +11 -4
- package/src/client/components/Link.vue +11 -5
- package/src/client/components/LocaleSwitcher.vue +5 -5
- package/src/client/components/PostDate.test.ts +82 -0
- package/src/client/components/PostDate.vue +32 -13
- package/src/client/components/PostItem.vue +17 -7
- package/src/client/components/PostList.vue +7 -4
- package/src/client/components/ProjectCard.vue +42 -14
- package/src/client/components/ProjectList.vue +12 -5
- package/src/client/components/RouterLink.vue +3 -3
- package/src/client/components/SharingWidget.vue +3 -1
- package/src/client/components/StatusBadge.vue +10 -10
- package/src/client/components/TermBadge.vue +11 -7
- package/src/client/components/TermLink.vue +4 -4
- package/src/client/composables/date.ts +1 -1
- package/src/client/composables/index.ts +1 -0
- package/src/client/composables/lang.test.ts +56 -0
- package/src/client/composables/lang.ts +65 -0
- package/src/client/composables/locale.ts +4 -1
- package/src/client/composables/project.ts +3 -3
- package/src/client/composables/rank.ts +5 -2
- package/src/client/index.ts +5 -0
- package/src/client/layouts/Layout.vue +2 -2
- package/src/client/pages/ArchivesPage.vue +10 -5
- package/src/client/pages/CategoriesPage.vue +13 -5
- package/src/client/pages/HomePage.vue +3 -3
- package/src/client/pages/Page.vue +13 -11
- package/src/client/pages/PostsPage.vue +4 -4
- package/src/client/pages/ProjectsPage.vue +10 -7
- package/src/client/pages/TagsPage.vue +10 -4
- package/src/client/plugins/donation/components/Donation.vue +5 -3
- package/src/client/plugins/donation/components/DonationButton.vue +3 -1
- package/src/client/plugins/donation/index.ts +11 -8
- package/src/client/plugins/i18n/index.ts +2 -2
- package/src/client/plugins/sharing/components/Sharing.vue +39 -15
- package/src/client/plugins/sharing/components/SharingButton.vue +34 -14
- package/src/client/plugins/sharing/index.ts +6 -2
- package/src/client/styles/base.css +30 -0
- package/src/client/styles/blog.css +376 -0
- package/src/client/styles/index.css +2 -30
- package/src/client/types/index.ts +8 -0
- package/src/node/composables/excerpt.test.ts +88 -0
- package/src/node/composables/excerpt.ts +57 -0
- package/src/node/composables/index.ts +1 -0
- package/src/node/composables/markdown.ts +8 -2
- package/src/node/composables/route.ts +15 -7
- package/src/node/composables/slug.ts +12 -4
- package/src/node/config.ts +20 -14
- package/src/shared/composables/useSlug.ts +2 -1
- package/src/vite-env.d.ts +1 -0
- package/dist/client/Donation-D9Xn0MPt.js +0 -76
- package/dist/client/GoogleAnalytics-Dp3EGn1x.js +0 -29
- package/dist/client/Sharing-ZrU7YF-x.js +0 -190
- package/dist/client/Swetrix-CM1mbD6X.js +0 -450
- package/dist/client/VPAlgoliaSearchBox-C18GEhLm.js +0 -126
- package/dist/client/VPCarbonAds-DwoamMao.js +0 -31
- package/dist/client/VPLocalSearchBox-DyteUinQ.js +0 -4086
- package/dist/client/index-BtT3qA6T.js +0 -12663
- package/dist/client/index-CCR5sUM8.js +0 -10688
- package/dist/client/index-CkmxezC0.js +0 -7616
- package/dist/client/index-DuYWs0vj.js +0 -265
- package/dist/node/composables/index.d.ts +0 -3
- package/dist/node/composables/markdown.d.ts +0 -2
- package/dist/node/composables/route.d.ts +0 -15
- package/dist/node/composables/slug.d.ts +0 -7
- package/dist/node/composables/types.d.ts +0 -7
- package/dist/node/config.d.ts +0 -45
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { computed, type PropType } from 'vue'
|
|
3
3
|
import { usePostDate } from '../composables'
|
|
4
4
|
import type { Post } from '../types'
|
|
@@ -7,8 +7,8 @@ import TermLink from './TermLink.vue'
|
|
|
7
7
|
const props = defineProps({
|
|
8
8
|
post: {
|
|
9
9
|
type: Object as PropType<Post>,
|
|
10
|
-
required: true
|
|
11
|
-
}
|
|
10
|
+
required: true
|
|
11
|
+
}
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
const postDate = computed(() => usePostDate(props.post.date))
|
|
@@ -16,18 +16,28 @@ const postDate = computed(() => usePostDate(props.post.date))
|
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
18
|
<div class="flex p-4 component-border">
|
|
19
|
-
<div class="text-center" style="min-width: 80px
|
|
19
|
+
<div class="text-center" style="min-width: 80px">
|
|
20
20
|
<div class="text-6xl font-bold">{{ postDate?.day }}</div>
|
|
21
21
|
<div>{{ postDate?.month }}, {{ postDate?.year }}</div>
|
|
22
22
|
</div>
|
|
23
23
|
<div class="pl-8">
|
|
24
24
|
<div class="flex flex-wrap mb-3">
|
|
25
|
-
<a
|
|
25
|
+
<a
|
|
26
|
+
class="text-2xl font-bold leading-8 tracking-tight"
|
|
27
|
+
:href="post?.url"
|
|
28
|
+
:title="post.title"
|
|
29
|
+
>{{ post.title }}</a
|
|
30
|
+
>
|
|
26
31
|
</div>
|
|
27
32
|
<div class="flex flex-wrap mb-3">
|
|
28
|
-
<TermLink
|
|
33
|
+
<TermLink
|
|
34
|
+
v-for="category in post.categories"
|
|
35
|
+
:key="category"
|
|
36
|
+
:text="category"
|
|
37
|
+
basePath="/categories/"
|
|
38
|
+
/>
|
|
29
39
|
</div>
|
|
30
|
-
<
|
|
40
|
+
<div v-html="post?.excerpt" />
|
|
31
41
|
</div>
|
|
32
42
|
</div>
|
|
33
43
|
</template>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import type { PropType } from 'vue'
|
|
3
3
|
import { useI18n } from 'vue-i18n'
|
|
4
4
|
import type { Post } from '../types'
|
|
@@ -8,15 +8,18 @@ defineProps({
|
|
|
8
8
|
posts: {
|
|
9
9
|
type: Object as PropType<Post[]>,
|
|
10
10
|
required: true,
|
|
11
|
-
default: []
|
|
12
|
-
}
|
|
11
|
+
default: []
|
|
12
|
+
}
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
const { t } = useI18n()
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<template>
|
|
19
|
-
<div
|
|
19
|
+
<div
|
|
20
|
+
v-if="posts.length > 0"
|
|
21
|
+
class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4"
|
|
22
|
+
>
|
|
20
23
|
<PostItem v-for="post of posts" :key="post?.url" :post="post" />
|
|
21
24
|
</div>
|
|
22
25
|
<p v-if="posts.length === 0">{{ t('no_post') }}</p>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { computed, type PropType } from 'vue'
|
|
3
3
|
import { useI18n } from 'vue-i18n'
|
|
4
4
|
import { useProjectStatusStyle } from '../composables'
|
|
@@ -11,28 +11,46 @@ import TermLink from './TermLink.vue'
|
|
|
11
11
|
const props = defineProps({
|
|
12
12
|
project: {
|
|
13
13
|
type: Object as PropType<Project>,
|
|
14
|
-
required: true
|
|
15
|
-
}
|
|
14
|
+
required: true
|
|
15
|
+
}
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
const { t } = useI18n()
|
|
19
|
-
const projectStatusStyles = computed(() =>
|
|
19
|
+
const projectStatusStyles = computed(() =>
|
|
20
|
+
useProjectStatusStyle(props.project.developmentStatus)
|
|
21
|
+
)
|
|
20
22
|
</script>
|
|
21
23
|
|
|
22
24
|
<template>
|
|
23
25
|
<div class="flex flex-col p-4 component-border">
|
|
24
26
|
<div class="flex flex-row justify-between items-center mb-4">
|
|
25
|
-
<a
|
|
27
|
+
<a
|
|
28
|
+
:href="project.url"
|
|
29
|
+
class="text-2xl font-bold leading-8 tracking-tight"
|
|
30
|
+
>
|
|
26
31
|
{{ project.title }}
|
|
27
32
|
</a>
|
|
28
33
|
<div class="flex flex-row">
|
|
29
|
-
<a
|
|
30
|
-
|
|
34
|
+
<a
|
|
35
|
+
v-if="project.showcaseUrl"
|
|
36
|
+
class="mx-1 text-gray-500 hover:text-gray-600"
|
|
37
|
+
target="_blank"
|
|
38
|
+
rel="noopener noreferrer"
|
|
39
|
+
:href="project.showcaseUrl"
|
|
40
|
+
>
|
|
31
41
|
<span class="sr-only">external-link</span>
|
|
32
|
-
<Icon
|
|
42
|
+
<Icon
|
|
43
|
+
icon="bi:box-arrow-up-right"
|
|
44
|
+
class="text-dark dark:text-white ml-2"
|
|
45
|
+
/>
|
|
33
46
|
</a>
|
|
34
|
-
<a
|
|
35
|
-
|
|
47
|
+
<a
|
|
48
|
+
v-if="project.repositoryUrl"
|
|
49
|
+
class="mx-1 text-gray-500 hover:text-gray-600"
|
|
50
|
+
target="_blank"
|
|
51
|
+
rel="noopener noreferrer"
|
|
52
|
+
:href="project.repositoryUrl"
|
|
53
|
+
>
|
|
36
54
|
<span class="sr-only">github</span>
|
|
37
55
|
<Icon icon="bi:github" class="text-dark dark:text-white ml-2" />
|
|
38
56
|
</a>
|
|
@@ -43,18 +61,28 @@ const projectStatusStyles = computed(() => useProjectStatusStyle(props.project.d
|
|
|
43
61
|
<div class="flex flex-col justify-between">
|
|
44
62
|
<div v-if="project.license" class="mb-3">
|
|
45
63
|
{{ t('license') }}:
|
|
46
|
-
<Link
|
|
47
|
-
|
|
64
|
+
<Link
|
|
65
|
+
v-if="project.licenseUrl"
|
|
66
|
+
:href="project.licenseUrl"
|
|
67
|
+
class="link-alt"
|
|
68
|
+
>
|
|
69
|
+
{{ project.license }}
|
|
48
70
|
</Link>
|
|
49
71
|
<span v-else>{{ project.license }}</span>
|
|
50
72
|
</div>
|
|
51
73
|
<div class="mb-3">
|
|
52
|
-
{{ t('development_status.title') }}:
|
|
74
|
+
{{ t('development_status.title') }}:
|
|
75
|
+
<StatusBadge v-bind="projectStatusStyles">
|
|
53
76
|
{{ $t(`development_status.${project.developmentStatus}`) }}
|
|
54
77
|
</StatusBadge>
|
|
55
78
|
</div>
|
|
56
79
|
<div class="flex flex-wrap mb-3">
|
|
57
|
-
<TermLink
|
|
80
|
+
<TermLink
|
|
81
|
+
v-for="tech in project.techs"
|
|
82
|
+
:key="tech"
|
|
83
|
+
:text="tech"
|
|
84
|
+
basePath="/projects?tech="
|
|
85
|
+
/>
|
|
58
86
|
</div>
|
|
59
87
|
<PostDate :date="project.date" text-size="sm" />
|
|
60
88
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import type { PropType } from 'vue'
|
|
3
3
|
import { useI18n } from 'vue-i18n'
|
|
4
4
|
import type { Project } from '../types'
|
|
@@ -8,16 +8,23 @@ defineProps({
|
|
|
8
8
|
projects: {
|
|
9
9
|
type: Object as PropType<Project[]>,
|
|
10
10
|
required: true,
|
|
11
|
-
default: []
|
|
12
|
-
}
|
|
11
|
+
default: []
|
|
12
|
+
}
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
const { t } = useI18n()
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<template>
|
|
19
|
-
<div
|
|
20
|
-
|
|
19
|
+
<div
|
|
20
|
+
v-if="projects.length > 0"
|
|
21
|
+
class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4"
|
|
22
|
+
>
|
|
23
|
+
<ProjectCard
|
|
24
|
+
v-for="project of projects"
|
|
25
|
+
:key="project?.url"
|
|
26
|
+
:project="project"
|
|
27
|
+
/>
|
|
21
28
|
</div>
|
|
22
29
|
<p v-if="projects.length === 0">{{ t('no_post') }}</p>
|
|
23
30
|
</template>
|
|
@@ -7,7 +7,9 @@ const route = useRoute()
|
|
|
7
7
|
const { theme, frontmatter, page } = useData()
|
|
8
8
|
|
|
9
9
|
const Sharing = theme.value.sharing
|
|
10
|
-
? defineAsyncComponent(
|
|
10
|
+
? defineAsyncComponent(
|
|
11
|
+
() => import('../plugins/sharing/components/Sharing.vue')
|
|
12
|
+
)
|
|
11
13
|
: () => null
|
|
12
14
|
|
|
13
15
|
const postURL = new URL(route.path, theme.value.siteURL).href
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
defineProps({
|
|
3
3
|
backgroundColor: {
|
|
4
4
|
type: String,
|
|
5
5
|
required: false,
|
|
6
|
-
default: 'bg-blue-100'
|
|
6
|
+
default: 'bg-blue-100'
|
|
7
7
|
},
|
|
8
8
|
darkBackgroundColor: {
|
|
9
9
|
type: String,
|
|
10
10
|
required: false,
|
|
11
|
-
default: 'dark:bg-blue-800'
|
|
11
|
+
default: 'dark:bg-blue-800'
|
|
12
12
|
},
|
|
13
13
|
textColor: {
|
|
14
14
|
type: String,
|
|
15
15
|
required: false,
|
|
16
|
-
default: 'text-blue-800'
|
|
16
|
+
default: 'text-blue-800'
|
|
17
17
|
},
|
|
18
18
|
darkTextColor: {
|
|
19
19
|
type: String,
|
|
20
20
|
required: false,
|
|
21
|
-
default: 'dark:text-blue-100'
|
|
21
|
+
default: 'dark:text-blue-100'
|
|
22
22
|
},
|
|
23
23
|
fontSize: {
|
|
24
24
|
type: String,
|
|
25
25
|
required: false,
|
|
26
|
-
default: 'text-base'
|
|
26
|
+
default: 'text-base'
|
|
27
27
|
},
|
|
28
28
|
fontWeight: {
|
|
29
29
|
type: String,
|
|
30
30
|
required: false,
|
|
31
|
-
default: 'font-normal'
|
|
31
|
+
default: 'font-normal'
|
|
32
32
|
},
|
|
33
33
|
rounded: {
|
|
34
34
|
type: Boolean,
|
|
35
35
|
required: false,
|
|
36
|
-
default: true
|
|
37
|
-
}
|
|
36
|
+
default: true
|
|
37
|
+
}
|
|
38
38
|
})
|
|
39
39
|
</script>
|
|
40
40
|
|
|
@@ -48,7 +48,7 @@ defineProps({
|
|
|
48
48
|
darkTextColor,
|
|
49
49
|
fontSize,
|
|
50
50
|
fontWeight,
|
|
51
|
-
rounded ? 'rounded' : ''
|
|
51
|
+
rounded ? 'rounded' : ''
|
|
52
52
|
]"
|
|
53
53
|
>
|
|
54
54
|
<slot />
|
|
@@ -4,25 +4,29 @@ import Link from './Link.vue'
|
|
|
4
4
|
defineProps({
|
|
5
5
|
title: {
|
|
6
6
|
type: String,
|
|
7
|
-
required: true
|
|
7
|
+
required: true
|
|
8
8
|
},
|
|
9
9
|
count: {
|
|
10
10
|
type: Number,
|
|
11
|
-
required: true
|
|
11
|
+
required: true
|
|
12
12
|
},
|
|
13
13
|
href: {
|
|
14
14
|
type: String,
|
|
15
|
-
required: true
|
|
16
|
-
}
|
|
15
|
+
required: true
|
|
16
|
+
}
|
|
17
17
|
})
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<template>
|
|
21
|
-
<Link
|
|
22
|
-
|
|
21
|
+
<Link
|
|
22
|
+
:key="title"
|
|
23
|
+
:href="href"
|
|
24
|
+
class="inline-flex items-center mt-2 mb-2 mr-2 px-5 py-2.5 text-sm font-medium text-center text-white component-border"
|
|
25
|
+
>
|
|
23
26
|
{{ title }}
|
|
24
27
|
<span
|
|
25
|
-
class="inline-flex items-center justify-center w-4 h-4 ms-2 text-xs font-semibold text-gray-900 dark:text-white bg-gray-300 dark:bg-gray-700 p-2.5 rounded"
|
|
28
|
+
class="inline-flex items-center justify-center w-4 h-4 ms-2 text-xs font-semibold text-gray-900 dark:text-white bg-gray-300 dark:bg-gray-700 p-2.5 rounded"
|
|
29
|
+
>
|
|
26
30
|
{{ count }}
|
|
27
31
|
</span>
|
|
28
32
|
</Link>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useSlug } from '../composables'
|
|
4
4
|
import RouterLink from './RouterLink.vue'
|
|
@@ -6,12 +6,12 @@ import RouterLink from './RouterLink.vue'
|
|
|
6
6
|
const props = defineProps({
|
|
7
7
|
text: {
|
|
8
8
|
type: String,
|
|
9
|
-
required: true
|
|
9
|
+
required: true
|
|
10
10
|
},
|
|
11
11
|
basePath: {
|
|
12
12
|
type: String,
|
|
13
|
-
required: true
|
|
14
|
-
}
|
|
13
|
+
required: true
|
|
14
|
+
}
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
const slug = computed(() => useSlug(props.text))
|
|
@@ -7,6 +7,6 @@ export const usePostDate: (raw: Date) => PostDate = (raw: Date) => {
|
|
|
7
7
|
time: +date,
|
|
8
8
|
day: date.toLocaleDateString('en-US', { day: 'numeric' }),
|
|
9
9
|
month: date.toLocaleDateString('en-US', { month: 'short' }),
|
|
10
|
-
year: date.toLocaleDateString('en-US', { year: 'numeric' })
|
|
10
|
+
year: date.toLocaleDateString('en-US', { year: 'numeric' })
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
2
|
+
import { nextTick, ref } from 'vue'
|
|
3
|
+
import { createI18n } from 'vue-i18n'
|
|
4
|
+
import { matchLocale, usePageLocale } from './lang'
|
|
5
|
+
|
|
6
|
+
const data = { frontmatter: ref<Record<string, unknown>>({}), lang: ref('en') }
|
|
7
|
+
vi.mock('vitepress', () => ({ useData: () => data }))
|
|
8
|
+
|
|
9
|
+
describe('matchLocale', () => {
|
|
10
|
+
const available = ['en', 'vi']
|
|
11
|
+
|
|
12
|
+
it('matches exactly', () => {
|
|
13
|
+
expect(matchLocale('vi', available)).toBe('vi')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('matches by base language and is case-insensitive', () => {
|
|
17
|
+
expect(matchLocale('vi-VN', available)).toBe('vi')
|
|
18
|
+
expect(matchLocale('EN-us', available)).toBe('en')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns undefined when unsupported', () => {
|
|
22
|
+
expect(matchLocale('fr', available)).toBeUndefined()
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('usePageLocale', () => {
|
|
27
|
+
const setup = () => {
|
|
28
|
+
const i18n = createI18n({
|
|
29
|
+
legacy: false,
|
|
30
|
+
locale: 'en',
|
|
31
|
+
messages: { en: {}, vi: {} }
|
|
32
|
+
})
|
|
33
|
+
usePageLocale(i18n)
|
|
34
|
+
return i18n
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('sets the locale from front matter lang immediately', () => {
|
|
38
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
39
|
+
expect(setup().global.locale.value).toBe('vi')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('keeps a manual choice until the page lang changes', async () => {
|
|
43
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
44
|
+
const i18n = setup()
|
|
45
|
+
i18n.global.locale.value = 'en'
|
|
46
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
47
|
+
await nextTick()
|
|
48
|
+
expect(i18n.global.locale.value).toBe('en')
|
|
49
|
+
data.frontmatter.value = {}
|
|
50
|
+
await nextTick()
|
|
51
|
+
expect(i18n.global.locale.value).toBe('en')
|
|
52
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
53
|
+
await nextTick()
|
|
54
|
+
expect(i18n.global.locale.value).toBe('vi')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useData } from 'vitepress'
|
|
2
|
+
import { type ComputedRef, computed, onMounted, watch, watchEffect } from 'vue'
|
|
3
|
+
import type { Locale } from 'vue-i18n'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolved language of the current page: `frontmatter.lang`, falling back to the site lang.
|
|
7
|
+
*/
|
|
8
|
+
export const usePageLang = (): ComputedRef<string> => {
|
|
9
|
+
const { frontmatter, lang } = useData()
|
|
10
|
+
return computed(() => {
|
|
11
|
+
const pageLang = frontmatter.value.lang
|
|
12
|
+
return typeof pageLang === 'string' && pageLang.trim()
|
|
13
|
+
? pageLang.trim()
|
|
14
|
+
: lang.value
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Keeps `<html lang>` in sync with the resolved page lang.
|
|
20
|
+
* VitePress resets it to the site lang on mount, so re-apply it after mount and on navigation.
|
|
21
|
+
*/
|
|
22
|
+
export const useHtmlLang = (): void => {
|
|
23
|
+
const pageLang = usePageLang()
|
|
24
|
+
// Registered inside onMounted so it runs after VitePress's own mount-time reset.
|
|
25
|
+
onMounted(() => {
|
|
26
|
+
watchEffect(
|
|
27
|
+
() => {
|
|
28
|
+
document.documentElement.lang = pageLang.value
|
|
29
|
+
},
|
|
30
|
+
{ flush: 'post' }
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Picks the available locale matching `lang` exactly or by base language (`vi-VN` -> `vi`).
|
|
37
|
+
*/
|
|
38
|
+
export const matchLocale = (
|
|
39
|
+
lang: string,
|
|
40
|
+
availableLocales: readonly Locale[]
|
|
41
|
+
): Locale | undefined => {
|
|
42
|
+
const normalized = lang.toLowerCase()
|
|
43
|
+
return (
|
|
44
|
+
availableLocales.find((l) => l.toLowerCase() === normalized) ??
|
|
45
|
+
availableLocales.find((l) => l.toLowerCase() === normalized.split('-')[0])
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Syncs the vue-i18n locale with the resolved page lang, synchronously so SSR renders the right strings.
|
|
51
|
+
* A manual `LocaleSwitcher` choice lasts until the page lang changes.
|
|
52
|
+
*/
|
|
53
|
+
export const usePageLocale = (i18n: {
|
|
54
|
+
global: { locale: { value: Locale }; availableLocales: Locale[] }
|
|
55
|
+
}): void => {
|
|
56
|
+
const pageLang = usePageLang()
|
|
57
|
+
watch(
|
|
58
|
+
pageLang,
|
|
59
|
+
(lang) => {
|
|
60
|
+
const matched = matchLocale(lang, i18n.global.availableLocales)
|
|
61
|
+
if (matched) i18n.global.locale.value = matched
|
|
62
|
+
},
|
|
63
|
+
{ immediate: true }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { WritableComputedRef } from 'vue'
|
|
2
2
|
import type { Locale } from 'vue-i18n'
|
|
3
3
|
|
|
4
|
-
export const switchLocale = (
|
|
4
|
+
export const switchLocale = (
|
|
5
|
+
availableLocales: string[],
|
|
6
|
+
locale: WritableComputedRef<Locale>
|
|
7
|
+
) => {
|
|
5
8
|
const locales = availableLocales
|
|
6
9
|
locale.value = locales[(locales.indexOf(locale.value) + 1) % locales.length]
|
|
7
10
|
}
|
|
@@ -6,7 +6,7 @@ export const useProjectStatusStyle = (developmentStatus: string) => {
|
|
|
6
6
|
backgroundColor: 'bg-green-100',
|
|
7
7
|
darkBackgroundColor: 'dark:bg-green-800',
|
|
8
8
|
textColor: 'text-green-800',
|
|
9
|
-
darkTextColor: 'dark:text-green-100'
|
|
9
|
+
darkTextColor: 'dark:text-green-100'
|
|
10
10
|
}
|
|
11
11
|
case 'inactive':
|
|
12
12
|
return {
|
|
@@ -14,7 +14,7 @@ export const useProjectStatusStyle = (developmentStatus: string) => {
|
|
|
14
14
|
backgroundColor: 'bg-yellow-100',
|
|
15
15
|
darkBackgroundColor: 'dark:bg-yellow-800',
|
|
16
16
|
textColor: 'text-yellow-800',
|
|
17
|
-
darkTextColor: 'dark:text-yellow-100'
|
|
17
|
+
darkTextColor: 'dark:text-yellow-100'
|
|
18
18
|
}
|
|
19
19
|
case 'unmaintained':
|
|
20
20
|
return {
|
|
@@ -22,7 +22,7 @@ export const useProjectStatusStyle = (developmentStatus: string) => {
|
|
|
22
22
|
backgroundColor: 'bg-red-100',
|
|
23
23
|
darkBackgroundColor: 'dark:bg-red-800',
|
|
24
24
|
textColor: 'text-red-800',
|
|
25
|
-
darkTextColor: 'dark:text-red-100'
|
|
25
|
+
darkTextColor: 'dark:text-red-100'
|
|
26
26
|
}
|
|
27
27
|
default:
|
|
28
28
|
return {}
|
|
@@ -4,10 +4,13 @@ export const useRank = <T extends string>(arr: T[]): Record<T, number> => {
|
|
|
4
4
|
acc[item] = (acc[item] || 0) + 1 // Increment the count if the key exists, otherwise initialize to 1
|
|
5
5
|
return acc
|
|
6
6
|
},
|
|
7
|
-
{} as Record<T, number
|
|
7
|
+
{} as Record<T, number>
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
const entries = Object.entries(rank) as [T, number][]
|
|
11
11
|
|
|
12
|
-
return Object.fromEntries(entries.sort(([, a], [, b]) => b - a)) as Record<
|
|
12
|
+
return Object.fromEntries(entries.sort(([, a], [, b]) => b - a)) as Record<
|
|
13
|
+
T,
|
|
14
|
+
number
|
|
15
|
+
>
|
|
13
16
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Icon } from '@iconify/vue'
|
|
2
2
|
import type { Theme } from 'vitepress'
|
|
3
3
|
import DefaultTheme from 'vitepress/theme'
|
|
4
|
+
import { useHtmlLang, usePageLocale } from './composables/lang'
|
|
4
5
|
import Layout from './layouts/Layout.vue'
|
|
5
6
|
import ArchivesPage from './pages/ArchivesPage.vue'
|
|
6
7
|
import CategoriesPage from './pages/CategoriesPage.vue'
|
|
@@ -29,6 +30,10 @@ const theme: Theme = {
|
|
|
29
30
|
app.component('ArchivesPage', ArchivesPage)
|
|
30
31
|
app.use(i18n)
|
|
31
32
|
},
|
|
33
|
+
setup() {
|
|
34
|
+
useHtmlLang()
|
|
35
|
+
usePageLocale(i18n)
|
|
36
|
+
}
|
|
32
37
|
} satisfies Theme
|
|
33
38
|
|
|
34
39
|
export * from './composables'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { useData, useRoute } from 'vitepress'
|
|
3
3
|
import DefaultTheme from 'vitepress/theme'
|
|
4
4
|
import { onMounted, watch } from 'vue'
|
|
@@ -30,7 +30,7 @@ watch(
|
|
|
30
30
|
if (newPath !== oldPath) {
|
|
31
31
|
initMediumZoom()
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
34
|
)
|
|
35
35
|
</script>
|
|
36
36
|
|
|
@@ -9,26 +9,31 @@ const props = defineProps({
|
|
|
9
9
|
posts: {
|
|
10
10
|
type: Object as PropType<Post[]>,
|
|
11
11
|
required: true,
|
|
12
|
-
default: []
|
|
13
|
-
}
|
|
12
|
+
default: []
|
|
13
|
+
}
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
const { t } = useI18n()
|
|
17
17
|
|
|
18
18
|
const totalPostsByYears = computed(() =>
|
|
19
|
-
useRank(
|
|
19
|
+
useRank(
|
|
20
|
+
props.posts.flatMap((post) => new Date(post.date).getFullYear().toString())
|
|
21
|
+
)
|
|
20
22
|
)
|
|
21
23
|
const sortedYearDesc = computed(() =>
|
|
22
24
|
Object.keys(totalPostsByYears.value)
|
|
23
25
|
.map((str) => Number(str))
|
|
24
|
-
.reverse()
|
|
26
|
+
.reverse()
|
|
25
27
|
)
|
|
26
28
|
</script>
|
|
27
29
|
|
|
28
30
|
<template>
|
|
29
31
|
<Page :title="t('archives')">
|
|
30
32
|
<div class="flex flex-wrap pt-4">
|
|
31
|
-
<div
|
|
33
|
+
<div
|
|
34
|
+
v-if="Object.keys(sortedYearDesc).length === 0"
|
|
35
|
+
class="mt-2 mb-2 mr-5"
|
|
36
|
+
>
|
|
32
37
|
{{ t('no_archive') }}
|
|
33
38
|
</div>
|
|
34
39
|
<TermBadge
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { computed, type PropType } from 'vue'
|
|
3
3
|
import { useI18n } from 'vue-i18n'
|
|
4
4
|
import TermBadge from '../components/TermBadge.vue'
|
|
@@ -9,13 +9,15 @@ const props = defineProps({
|
|
|
9
9
|
posts: {
|
|
10
10
|
type: Object as PropType<Post[]>,
|
|
11
11
|
required: true,
|
|
12
|
-
default: []
|
|
13
|
-
}
|
|
12
|
+
default: []
|
|
13
|
+
}
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
const { t } = useI18n()
|
|
17
17
|
|
|
18
|
-
const categories = computed(() =>
|
|
18
|
+
const categories = computed(() =>
|
|
19
|
+
useRank(props.posts.flatMap((post) => post.categories))
|
|
20
|
+
)
|
|
19
21
|
</script>
|
|
20
22
|
|
|
21
23
|
<template>
|
|
@@ -24,7 +26,13 @@ const categories = computed(() => useRank(props.posts.flatMap((post) => post.cat
|
|
|
24
26
|
<div v-if="Object.keys(categories).length === 0" class="mt-2 mb-2 mr-5">
|
|
25
27
|
{{ t('no_category') }}
|
|
26
28
|
</div>
|
|
27
|
-
<TermBadge
|
|
29
|
+
<TermBadge
|
|
30
|
+
v-for="(count, category) in categories"
|
|
31
|
+
:key="category"
|
|
32
|
+
:title="category"
|
|
33
|
+
:count="count"
|
|
34
|
+
:href="`/categories/${useSlug(category)}`"
|
|
35
|
+
/>
|
|
28
36
|
</div>
|
|
29
37
|
</Page>
|
|
30
38
|
</template>
|