@ansidev-oss/vitepress-theme-ansidev 1.0.6
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/LICENSE +21 -0
- package/dist/client/Donation-D9Xn0MPt.js +76 -0
- package/dist/client/GoogleAnalytics-Dp3EGn1x.js +29 -0
- package/dist/client/VPAlgoliaSearchBox-DXE-LCVf.js +126 -0
- package/dist/client/VPCarbonAds-Czmm53YE.js +31 -0
- package/dist/client/VPLocalSearchBox-ihwA4uH-.js +4086 -0
- package/dist/client/composables/date.d.ts +2 -0
- package/dist/client/composables/index.d.ts +4 -0
- package/dist/client/composables/locale.d.ts +3 -0
- package/dist/client/composables/project.d.ts +13 -0
- package/dist/client/composables/rank.d.ts +1 -0
- package/dist/client/composables/slug.d.ts +2 -0
- package/dist/client/index-BtT3qA6T.js +12663 -0
- package/dist/client/index-CCR5sUM8.js +10688 -0
- package/dist/client/index-CZCdwVUW.js +7566 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +5 -0
- package/dist/client/plugins/donation/index.d.ts +19 -0
- package/dist/client/plugins/google-analytics/index.d.ts +8 -0
- package/dist/client/plugins/i18n/index.d.ts +53 -0
- package/dist/client/styles.css +1 -0
- package/dist/client/types/index.d.ts +45 -0
- package/dist/node/composables.d.mts +30 -0
- package/dist/node/composables.mjs +51 -0
- package/dist/node/config.d.mts +46 -0
- package/dist/node/config.mjs +47 -0
- package/package.json +87 -0
- package/src/client/components/EmptyFooter.vue +3 -0
- package/src/client/components/Footer.vue +87 -0
- package/src/client/components/Link.vue +31 -0
- package/src/client/components/LocaleSwitcher.vue +59 -0
- package/src/client/components/PostDate.vue +42 -0
- package/src/client/components/PostItem.vue +33 -0
- package/src/client/components/PostList.vue +23 -0
- package/src/client/components/ProjectCard.vue +68 -0
- package/src/client/components/ProjectList.vue +23 -0
- package/src/client/components/RouterLink.vue +19 -0
- package/src/client/components/StatusBadge.vue +56 -0
- package/src/client/components/TermBadge.vue +29 -0
- package/src/client/components/TermLink.vue +25 -0
- package/src/client/composables/date.ts +12 -0
- package/src/client/composables/index.ts +4 -0
- package/src/client/composables/locale.ts +7 -0
- package/src/client/composables/project.ts +30 -0
- package/src/client/composables/rank.ts +13 -0
- package/src/client/composables/slug.ts +12 -0
- package/src/client/index.ts +43 -0
- package/src/client/layouts/Layout.vue +25 -0
- package/src/client/pages/ArchivesPage.vue +43 -0
- package/src/client/pages/CategoriesPage.vue +30 -0
- package/src/client/pages/HomePage.vue +18 -0
- package/src/client/pages/Page.vue +20 -0
- package/src/client/pages/PostsPage.vue +23 -0
- package/src/client/pages/ProjectsPage.vue +65 -0
- package/src/client/pages/TagsPage.vue +30 -0
- package/src/client/plugins/donation/components/Donation.vue +43 -0
- package/src/client/plugins/donation/components/DonationButton.vue +22 -0
- package/src/client/plugins/donation/index.ts +42 -0
- package/src/client/plugins/google-analytics/components/GoogleAnalytics.vue +52 -0
- package/src/client/plugins/google-analytics/index.ts +8 -0
- package/src/client/plugins/i18n/index.ts +13 -0
- package/src/client/plugins/i18n/locales/en.json +25 -0
- package/src/client/plugins/i18n/locales/vi.json +25 -0
- package/src/client/styles/index.css +30 -0
- package/src/client/types/index.ts +50 -0
- package/src/node/composables/index.ts +3 -0
- package/src/node/composables/markdown.ts +14 -0
- package/src/node/composables/route.ts +26 -0
- package/src/node/composables/slug.ts +40 -0
- package/src/node/composables/types.ts +7 -0
- package/src/node/config.ts +79 -0
- package/src/vite-env.d.ts +7 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DefaultTheme } from 'vitepress/theme';
|
|
2
|
+
import type { DonationPluginConfig } from '../plugins/donation';
|
|
3
|
+
import type { GoogleAnalyticsOptions } from '../plugins/google-analytics';
|
|
4
|
+
export interface ThemeConfig extends DefaultTheme.Config {
|
|
5
|
+
/**
|
|
6
|
+
* The site URL.
|
|
7
|
+
*
|
|
8
|
+
* @default process.env.VITE_BASE_URL
|
|
9
|
+
*/
|
|
10
|
+
siteURL?: string;
|
|
11
|
+
googleAnalytics?: GoogleAnalyticsOptions;
|
|
12
|
+
donation?: DonationPluginConfig;
|
|
13
|
+
}
|
|
14
|
+
export type { DonationPluginConfig } from '../plugins/donation';
|
|
15
|
+
export type { GoogleAnalyticsOptions } from '../plugins/google-analytics';
|
|
16
|
+
export interface Post {
|
|
17
|
+
type: string;
|
|
18
|
+
title: string;
|
|
19
|
+
author: string;
|
|
20
|
+
gravatar: string;
|
|
21
|
+
twitter: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
date: Date;
|
|
24
|
+
excerpt: string | undefined;
|
|
25
|
+
categories: string[];
|
|
26
|
+
tags: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface PostDate {
|
|
29
|
+
time: number;
|
|
30
|
+
day: string;
|
|
31
|
+
month: string;
|
|
32
|
+
year: string;
|
|
33
|
+
}
|
|
34
|
+
export interface Project {
|
|
35
|
+
title: string;
|
|
36
|
+
url: string;
|
|
37
|
+
showcaseUrl: string;
|
|
38
|
+
repositoryUrl: string;
|
|
39
|
+
excerpt: string | undefined;
|
|
40
|
+
license: string;
|
|
41
|
+
licenseUrl: string;
|
|
42
|
+
developmentStatus: string;
|
|
43
|
+
techs: string[];
|
|
44
|
+
date: Date;
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/node/composables/types.d.ts
|
|
2
|
+
type Pattern = string | string[];
|
|
3
|
+
type Frontmatter = Record<string, unknown> & {
|
|
4
|
+
date?: string;
|
|
5
|
+
excerpt?: string;
|
|
6
|
+
};
|
|
7
|
+
type FrontmatterToString = (frontmatter: Frontmatter) => string;
|
|
8
|
+
type StringOrFrontmatterToString = string | FrontmatterToString;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/node/composables/markdown.d.ts
|
|
11
|
+
declare const useMarkdownFrontmatter: (pattern: Pattern, excerptSeparator?: string) => Array<Frontmatter>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/node/composables/route.d.ts
|
|
14
|
+
declare const useMarkdownFrontmatterRoute: (pattern: Pattern, frontmatterKey: string, excerptSeparator?: string) => {
|
|
15
|
+
paths(): any;
|
|
16
|
+
};
|
|
17
|
+
declare const useArchiveRoute: (pattern: Pattern, excerptSeparator?: string) => {
|
|
18
|
+
paths(): any;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/node/composables/slug.d.ts
|
|
22
|
+
declare const useSlug: (str: string) => string;
|
|
23
|
+
declare const useSlugFilter: (slug: string) => (str: string) => boolean;
|
|
24
|
+
declare const useSlugFromMarkdownFrontMatter: (pattern: Pattern, frontmatterKeyOrMappingFn: StringOrFrontmatterToString, excerptSeparator?: string) => {
|
|
25
|
+
params: {
|
|
26
|
+
slug: string;
|
|
27
|
+
};
|
|
28
|
+
}[];
|
|
29
|
+
//#endregion
|
|
30
|
+
export { useArchiveRoute, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { globSync } from "glob";
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
|
|
6
|
+
//#region src/node/composables/markdown.ts
|
|
7
|
+
const useMarkdownFrontmatter = (pattern, excerptSeparator = "---") => {
|
|
8
|
+
return globSync(pattern, { ignore: ["node_modules/**", ".git/**"] }).map((file) => {
|
|
9
|
+
const { data, excerpt } = matter(fs.readFileSync(resolve(file), "utf-8"), {
|
|
10
|
+
excerpt: true,
|
|
11
|
+
excerpt_separator: excerptSeparator
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
...data,
|
|
15
|
+
excerpt
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/node/composables/slug.ts
|
|
22
|
+
const useSlug = (str) => {
|
|
23
|
+
return str.replace(/#/, "-sharp-").replace(/\./, "-dot-").replace(/-$/, "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
24
|
+
};
|
|
25
|
+
const useSlugFilter = (slug) => (str) => useSlug(str) === slug;
|
|
26
|
+
const useSlugFromMarkdownFrontMatter = (pattern, frontmatterKeyOrMappingFn, excerptSeparator = "---") => {
|
|
27
|
+
const frontMatters = useMarkdownFrontmatter(pattern, excerptSeparator);
|
|
28
|
+
const _frontmatterMappingFn = typeof frontmatterKeyOrMappingFn === "string" ? (frontMatter) => frontMatter[frontmatterKeyOrMappingFn] : frontmatterKeyOrMappingFn;
|
|
29
|
+
const arr = frontMatters.flatMap(_frontmatterMappingFn);
|
|
30
|
+
return [...new Set(arr)].map((str) => {
|
|
31
|
+
const slug = useSlug(str);
|
|
32
|
+
if ("index" === slug) console.warn("[WARN] Generated routes contain the reserved route 'index'.");
|
|
33
|
+
return { params: { slug } };
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/node/composables/route.ts
|
|
39
|
+
const useMarkdownFrontmatterRoute = (pattern, frontmatterKey, excerptSeparator = "---") => {
|
|
40
|
+
return { paths() {
|
|
41
|
+
return useSlugFromMarkdownFrontMatter(pattern, frontmatterKey, excerptSeparator);
|
|
42
|
+
} };
|
|
43
|
+
};
|
|
44
|
+
const useArchiveRoute = (pattern, excerptSeparator = "---") => {
|
|
45
|
+
return { paths() {
|
|
46
|
+
return useSlugFromMarkdownFrontMatter(pattern, (frontmatter) => new Date(frontmatter.date).getFullYear().toString(), excerptSeparator);
|
|
47
|
+
} };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { useArchiveRoute, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/node/config.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* @type {import('vitepress').UserConfig}
|
|
4
|
+
*/
|
|
5
|
+
declare const config: {
|
|
6
|
+
themeConfig: {
|
|
7
|
+
siteURL: string;
|
|
8
|
+
googleAnalytics: {
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
search: {
|
|
12
|
+
provider: string;
|
|
13
|
+
options: {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} src
|
|
16
|
+
* @param {import('vitepress').MarkdownEnv} env
|
|
17
|
+
* @param {import('markdown-it')} md
|
|
18
|
+
*/
|
|
19
|
+
_render(src: any, env: any, md: any): Promise<string>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
vite: {
|
|
24
|
+
ssr: {
|
|
25
|
+
noExternal: string[];
|
|
26
|
+
};
|
|
27
|
+
optimizeDeps: {
|
|
28
|
+
exclude: string[];
|
|
29
|
+
};
|
|
30
|
+
css: {
|
|
31
|
+
preprocessorOptions: {
|
|
32
|
+
scss: {
|
|
33
|
+
api: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
resolve: {
|
|
38
|
+
alias: {
|
|
39
|
+
find: RegExp;
|
|
40
|
+
replacement: string;
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { config as default };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { URL, fileURLToPath } from "node:url";
|
|
2
|
+
import { loadEnv } from "vitepress";
|
|
3
|
+
|
|
4
|
+
//#region src/node/config.ts
|
|
5
|
+
process.env.VITE_EXTRA_EXTENSIONS = "rss";
|
|
6
|
+
globalThis.__VUE_PROD_DEVTOOLS__ = process.env.NODE_ENV === "development";
|
|
7
|
+
const env = loadEnv(process.env.NODE_ENV || "development", process.cwd(), "VITE_");
|
|
8
|
+
const siteURL = env.VITE_BASE_URL;
|
|
9
|
+
/**
|
|
10
|
+
* This file is intended to be required from VitePress
|
|
11
|
+
* consuming project's config file.
|
|
12
|
+
*
|
|
13
|
+
* It runs in Node.js.
|
|
14
|
+
*/
|
|
15
|
+
const deps = ["vitepress-theme-ansidev"];
|
|
16
|
+
/**
|
|
17
|
+
* @type {import('vitepress').UserConfig}
|
|
18
|
+
*/
|
|
19
|
+
const config = {
|
|
20
|
+
themeConfig: {
|
|
21
|
+
siteURL,
|
|
22
|
+
googleAnalytics: { id: env.VITE_GA_ID },
|
|
23
|
+
search: {
|
|
24
|
+
provider: "local",
|
|
25
|
+
options: { async _render(src, env, md) {
|
|
26
|
+
const html = await md.renderAsync(src, env);
|
|
27
|
+
if (env.frontmatter?.search === false) return "";
|
|
28
|
+
let renderedHtml = "";
|
|
29
|
+
if (env.frontmatter?.title) renderedHtml += `<h1>${env.frontmatter.title} <a href="#">​</a></h1>`;
|
|
30
|
+
if (html.length > 0) renderedHtml += html;
|
|
31
|
+
return renderedHtml;
|
|
32
|
+
} }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
vite: {
|
|
36
|
+
ssr: { noExternal: deps },
|
|
37
|
+
optimizeDeps: { exclude: deps },
|
|
38
|
+
css: { preprocessorOptions: { scss: { api: "modern-compiler" } } },
|
|
39
|
+
resolve: { alias: [{
|
|
40
|
+
find: /^.*\/VPFooter\.vue$/,
|
|
41
|
+
replacement: fileURLToPath(new URL("./components/EmptyFooter.vue", import.meta.url))
|
|
42
|
+
}] }
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { config as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ansidev-oss/vitepress-theme-ansidev",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "The VitePress theme for ansidev's blog",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/node/index.js",
|
|
7
|
+
"types": "types/index.d.ts",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Le Minh Tri <ansidev@gmail.com>",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/ansidev/vitepress-theme-ansidev.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"vitepress",
|
|
16
|
+
"theme",
|
|
17
|
+
"blog",
|
|
18
|
+
"tailwindcss",
|
|
19
|
+
"vue3",
|
|
20
|
+
"typescript",
|
|
21
|
+
"i18n",
|
|
22
|
+
"dark-mode",
|
|
23
|
+
"ansidev"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/client/index.js",
|
|
28
|
+
"types": "./dist/client/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./styles.css": "./dist/client/styles.css",
|
|
31
|
+
"./config": {
|
|
32
|
+
"import": "./dist/node/config.mjs",
|
|
33
|
+
"types": "./dist/node/config.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./composables": {
|
|
36
|
+
"import": "./dist/node/composables.mjs",
|
|
37
|
+
"types": "./dist/node/composables.d.ts"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"src",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@tailwindcss/vite": "^4.2.0",
|
|
48
|
+
"@vueuse/core": "^14.2.1",
|
|
49
|
+
"vue-i18n": "^11.2.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@biomejs/biome": "2.4.2",
|
|
53
|
+
"@iconify/vue": "^5.0.0",
|
|
54
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
55
|
+
"@types/node": "^25.2.3",
|
|
56
|
+
"@vitejs/plugin-vue": "^6.0.4",
|
|
57
|
+
"autoprefixer": "^10.4.24",
|
|
58
|
+
"glob": "^13.0.5",
|
|
59
|
+
"gray-matter": "^4.0.3",
|
|
60
|
+
"postcss": "^8.5.6",
|
|
61
|
+
"prettier": "^3.8.1",
|
|
62
|
+
"rimraf": "^6.1.3",
|
|
63
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
64
|
+
"tailwindcss": "^4.1.18",
|
|
65
|
+
"ts-node": "^10.9.2",
|
|
66
|
+
"tsdown": "^0.20.3",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"vite": "^7.3.1",
|
|
69
|
+
"vitepress": "^2.0.0-alpha.16",
|
|
70
|
+
"vue": "^3.5.28",
|
|
71
|
+
"vue-tsc": "^3.2.4"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"vitepress": "^2.0.0-alpha.16",
|
|
75
|
+
"vue": "^3.5.28"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"typecheck": "tsc --noEmit",
|
|
79
|
+
"lint:check": "biome check",
|
|
80
|
+
"lint": "biome check --write",
|
|
81
|
+
"dev": "vite build --watch",
|
|
82
|
+
"clean": "rimraf dist",
|
|
83
|
+
"build:client": "vite build && tsc --emitDeclarationOnly",
|
|
84
|
+
"build:node": "tsdown",
|
|
85
|
+
"build": "pnpm clean && pnpm build:client && pnpm build:node"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useData } from 'vitepress'
|
|
3
|
+
import { useLayout } from 'vitepress/theme'
|
|
4
|
+
import { computed, defineAsyncComponent } from 'vue'
|
|
5
|
+
|
|
6
|
+
const { theme, frontmatter } = useData()
|
|
7
|
+
const { hasSidebar } = useLayout()
|
|
8
|
+
|
|
9
|
+
const copyright = computed(() => {
|
|
10
|
+
return typeof theme.value.footer.copyright === 'string'
|
|
11
|
+
? theme.value.footer.copyright.replace(/#\{present\}/g, new Date().getFullYear().toString())
|
|
12
|
+
: theme.value.footer.copyright
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const GoogleAnalytics = theme.value.googleAnalytics
|
|
16
|
+
? defineAsyncComponent(() => import('../plugins/google-analytics/components/GoogleAnalytics.vue'))
|
|
17
|
+
: () => null
|
|
18
|
+
|
|
19
|
+
const Donation = theme.value.donation
|
|
20
|
+
? defineAsyncComponent(() => import('../plugins/donation/components/Donation.vue'))
|
|
21
|
+
: () => null
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="flex flex-wrap space-x-4 py-4 items-center justify-center">
|
|
26
|
+
<Donation :donation="theme.donation" />
|
|
27
|
+
</div>
|
|
28
|
+
<footer
|
|
29
|
+
v-if="theme.footer && frontmatter.footer !== false"
|
|
30
|
+
class="VPFooter"
|
|
31
|
+
:class="{ 'has-sidebar': hasSidebar }"
|
|
32
|
+
>
|
|
33
|
+
<div class="container">
|
|
34
|
+
<p
|
|
35
|
+
v-if="theme.footer.message"
|
|
36
|
+
class="message"
|
|
37
|
+
v-html="theme.footer.message"
|
|
38
|
+
></p>
|
|
39
|
+
<p v-if="copyright" class="copyright" v-html="copyright"></p>
|
|
40
|
+
</div>
|
|
41
|
+
</footer>
|
|
42
|
+
<GoogleAnalytics :google-analytics="theme.googleAnalytics" />
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
.VPFooter {
|
|
47
|
+
position: relative;
|
|
48
|
+
z-index: var(--vp-z-index-footer);
|
|
49
|
+
border-top: 1px solid var(--vp-c-gutter);
|
|
50
|
+
padding: 32px 24px;
|
|
51
|
+
background-color: var(--vp-c-bg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.VPFooter.has-sidebar {
|
|
55
|
+
display: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.VPFooter :deep(a) {
|
|
59
|
+
text-decoration-line: underline;
|
|
60
|
+
text-underline-offset: 2px;
|
|
61
|
+
transition: color 0.25s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.VPFooter :deep(a:hover) {
|
|
65
|
+
color: var(--vp-c-text-1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (min-width: 768px) {
|
|
69
|
+
.VPFooter {
|
|
70
|
+
padding: 32px;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.container {
|
|
75
|
+
margin: 0 auto;
|
|
76
|
+
max-width: var(--vp-layout-max-width);
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.message,
|
|
81
|
+
.copyright {
|
|
82
|
+
line-height: 24px;
|
|
83
|
+
font-size: 14px;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
color: var(--vp-c-text-2);
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import RouterLink from './RouterLink.vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
href: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
default: '#',
|
|
9
|
+
},
|
|
10
|
+
disableRouterLink: {
|
|
11
|
+
type: Boolean,
|
|
12
|
+
required: false,
|
|
13
|
+
default: false,
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const isInternalLink = props.href?.startsWith('/') && !props.disableRouterLink
|
|
18
|
+
const isAnchorLink = props.href?.startsWith('#')
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<RouterLink v-if="isInternalLink || isAnchorLink" :to="href" v-bind="$attrs">
|
|
23
|
+
<slot />
|
|
24
|
+
</RouterLink>
|
|
25
|
+
<a v-else-if="disableRouterLink" target="_self" :href="href" v-bind="$attrs">
|
|
26
|
+
<slot />
|
|
27
|
+
</a>
|
|
28
|
+
<a v-else target="_blank" rel="noopener noreferrer" :href="href" v-bind="$attrs">
|
|
29
|
+
<slot />
|
|
30
|
+
</a>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from 'vue-i18n'
|
|
3
|
+
import { switchLocale } from '../composables/locale'
|
|
4
|
+
|
|
5
|
+
defineProps({
|
|
6
|
+
size: {
|
|
7
|
+
type: Number,
|
|
8
|
+
required: false,
|
|
9
|
+
default: 20,
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { t, availableLocales, locale } = useI18n()
|
|
14
|
+
const toggleLocale = () => switchLocale(availableLocales, locale)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="divider" />
|
|
19
|
+
<button
|
|
20
|
+
:aria-label="t('button.toggle_locale')"
|
|
21
|
+
type="button"
|
|
22
|
+
class="VPLocaleSwitcher"
|
|
23
|
+
@click="toggleLocale"
|
|
24
|
+
>
|
|
25
|
+
<Icon icon="bi:translate" :width="size" :height="size" />
|
|
26
|
+
</button>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.divider {
|
|
31
|
+
margin-right: 8px;
|
|
32
|
+
margin-left: 16px;
|
|
33
|
+
width: 1px;
|
|
34
|
+
height: 24px;
|
|
35
|
+
background-color: var(--vp-c-divider);
|
|
36
|
+
content: "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.VPLocaleSwitcher {
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
align-items: center;
|
|
43
|
+
width: 36px;
|
|
44
|
+
height: 36px;
|
|
45
|
+
color: var(--vp-c-text-2);
|
|
46
|
+
transition: color 0.5s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.VPLocaleSwitcher:hover {
|
|
50
|
+
color: var(--vp-c-text-1);
|
|
51
|
+
transition: color 0.25s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.VPLocaleSwitcher > :deep(svg) {
|
|
55
|
+
width: 20px;
|
|
56
|
+
height: 20px;
|
|
57
|
+
fill: currentColor;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useI18n } from 'vue-i18n'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
/**
|
|
7
|
+
* { time, string }
|
|
8
|
+
*/
|
|
9
|
+
date: [Date, String],
|
|
10
|
+
textSize: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: null,
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
const dateObject = computed(() => {
|
|
18
|
+
const date = props.date instanceof Date ? props.date : new Date(props.date as string)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
time: +date,
|
|
22
|
+
string: date.toLocaleDateString('en-US', {
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'long',
|
|
25
|
+
day: 'numeric',
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const getDateTime = () => new Date().toISOString()
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<dl>
|
|
35
|
+
<dt class="sr-only">
|
|
36
|
+
{{ t('posted_on') }}
|
|
37
|
+
</dt>
|
|
38
|
+
<dd class="font-medium leading-6" :class="[textSize ? `text-${textSize}` : 'text-base']">
|
|
39
|
+
<time :datetime="getDateTime()">{{ dateObject.string }}</time>
|
|
40
|
+
</dd>
|
|
41
|
+
</dl>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import { computed, type PropType } from 'vue'
|
|
3
|
+
import { usePostDate } from '../composables'
|
|
4
|
+
import type { Post } from '../types'
|
|
5
|
+
import TermLink from './TermLink.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
post: {
|
|
9
|
+
type: Object as PropType<Post>,
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const postDate = computed(() => usePostDate(props.post.date))
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="flex p-4 component-border">
|
|
19
|
+
<div class="text-center" style="min-width: 80px;">
|
|
20
|
+
<div class="text-6xl font-bold">{{ postDate?.day }}</div>
|
|
21
|
+
<div>{{ postDate?.month }}, {{ postDate?.year }}</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="pl-8">
|
|
24
|
+
<div class="flex flex-wrap mb-3">
|
|
25
|
+
<a class="text-2xl font-bold leading-8 tracking-tight" :href="post?.url" :title="post.title">{{ post.title }}</a>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="flex flex-wrap mb-3">
|
|
28
|
+
<TermLink v-for="category in post.categories" :key="category" :text="category" basePath="/categories/" />
|
|
29
|
+
</div>
|
|
30
|
+
<p>{{ post?.excerpt }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import { useI18n } from 'vue-i18n'
|
|
4
|
+
import type { Post } from '../types'
|
|
5
|
+
import PostItem from './PostItem.vue'
|
|
6
|
+
|
|
7
|
+
defineProps({
|
|
8
|
+
posts: {
|
|
9
|
+
type: Object as PropType<Post[]>,
|
|
10
|
+
required: true,
|
|
11
|
+
default: [],
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const { t } = useI18n()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div v-if="posts.length > 0" class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
|
|
20
|
+
<PostItem v-for="post of posts" :key="post?.url" :post="post" />
|
|
21
|
+
</div>
|
|
22
|
+
<p v-if="posts.length === 0">{{ t('no_post') }}</p>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import { computed, type PropType } from 'vue'
|
|
3
|
+
import { useI18n } from 'vue-i18n'
|
|
4
|
+
import { useProjectStatusStyle } from '../composables'
|
|
5
|
+
import type { Project } from '../types'
|
|
6
|
+
import Link from './Link.vue'
|
|
7
|
+
import PostDate from './PostDate.vue'
|
|
8
|
+
import StatusBadge from './StatusBadge.vue'
|
|
9
|
+
import TermLink from './TermLink.vue'
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
project: {
|
|
13
|
+
type: Object as PropType<Project>,
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n()
|
|
19
|
+
const projectStatusStyles = computed(() => useProjectStatusStyle(props.project.developmentStatus))
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="flex flex-col p-4 component-border">
|
|
24
|
+
<div class="flex flex-row justify-between items-center mb-4">
|
|
25
|
+
<a :href="project.url" class="text-2xl font-bold leading-8 tracking-tight">
|
|
26
|
+
{{ project.title }}
|
|
27
|
+
</a>
|
|
28
|
+
<div class="flex flex-row">
|
|
29
|
+
<a v-if="project.showcaseUrl" class="mx-1 text-gray-500 transition hover:text-gray-600" target="_blank"
|
|
30
|
+
rel="noopener noreferrer" :href="project.showcaseUrl">
|
|
31
|
+
<span class="sr-only">external-link</span>
|
|
32
|
+
<Icon icon="bi:box-arrow-up-right" class="text-dark dark:text-white ml-2" />
|
|
33
|
+
</a>
|
|
34
|
+
<a v-if="project.repositoryUrl" class="mx-1 text-gray-500 transition hover:text-gray-600" target="_blank"
|
|
35
|
+
rel="noopener noreferrer" :href="project.repositoryUrl">
|
|
36
|
+
<span class="sr-only">github</span>
|
|
37
|
+
<Icon icon="bi:github" class="text-dark dark:text-white ml-2" />
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="project-info">
|
|
42
|
+
<div v-html="project.excerpt" />
|
|
43
|
+
<div class="flex flex-col justify-between">
|
|
44
|
+
<div v-if="project.license" class="mb-3">
|
|
45
|
+
{{ t('license') }}:
|
|
46
|
+
<Link v-if="project.licenseUrl" :href="project.licenseUrl" class="link-alt">
|
|
47
|
+
{{ project.license }}
|
|
48
|
+
</Link>
|
|
49
|
+
<span v-else>{{ project.license }}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="mb-3">
|
|
52
|
+
{{ t('development_status.title') }}: <StatusBadge v-bind="projectStatusStyles">
|
|
53
|
+
{{ $t(`development_status.${project.developmentStatus}`) }}
|
|
54
|
+
</StatusBadge>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex flex-wrap mb-3">
|
|
57
|
+
<TermLink v-for="tech in project.techs" :key="tech" :text="tech" basePath="/projects?tech=" />
|
|
58
|
+
</div>
|
|
59
|
+
<PostDate :date="project.date" text-size="sm" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="text-end">
|
|
63
|
+
<a :href="project.url" :aria-label="`Read ${project.title}`">
|
|
64
|
+
{{ t('button.read_more') }} →
|
|
65
|
+
</a>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import { useI18n } from 'vue-i18n'
|
|
4
|
+
import type { Project } from '../types'
|
|
5
|
+
import ProjectCard from './ProjectCard.vue'
|
|
6
|
+
|
|
7
|
+
defineProps({
|
|
8
|
+
projects: {
|
|
9
|
+
type: Object as PropType<Project[]>,
|
|
10
|
+
required: true,
|
|
11
|
+
default: [],
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const { t } = useI18n()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div v-if="projects.length > 0" class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
|
|
20
|
+
<ProjectCard v-for="project of projects" :key="project?.url" :project="project" />
|
|
21
|
+
</div>
|
|
22
|
+
<p v-if="projects.length === 0">{{ t('no_post') }}</p>
|
|
23
|
+
</template>
|