@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/dist/client/Donation-D9Xn0MPt.js +76 -0
  3. package/dist/client/GoogleAnalytics-Dp3EGn1x.js +29 -0
  4. package/dist/client/VPAlgoliaSearchBox-DXE-LCVf.js +126 -0
  5. package/dist/client/VPCarbonAds-Czmm53YE.js +31 -0
  6. package/dist/client/VPLocalSearchBox-ihwA4uH-.js +4086 -0
  7. package/dist/client/composables/date.d.ts +2 -0
  8. package/dist/client/composables/index.d.ts +4 -0
  9. package/dist/client/composables/locale.d.ts +3 -0
  10. package/dist/client/composables/project.d.ts +13 -0
  11. package/dist/client/composables/rank.d.ts +1 -0
  12. package/dist/client/composables/slug.d.ts +2 -0
  13. package/dist/client/index-BtT3qA6T.js +12663 -0
  14. package/dist/client/index-CCR5sUM8.js +10688 -0
  15. package/dist/client/index-CZCdwVUW.js +7566 -0
  16. package/dist/client/index.d.ts +6 -0
  17. package/dist/client/index.js +5 -0
  18. package/dist/client/plugins/donation/index.d.ts +19 -0
  19. package/dist/client/plugins/google-analytics/index.d.ts +8 -0
  20. package/dist/client/plugins/i18n/index.d.ts +53 -0
  21. package/dist/client/styles.css +1 -0
  22. package/dist/client/types/index.d.ts +45 -0
  23. package/dist/node/composables.d.mts +30 -0
  24. package/dist/node/composables.mjs +51 -0
  25. package/dist/node/config.d.mts +46 -0
  26. package/dist/node/config.mjs +47 -0
  27. package/package.json +87 -0
  28. package/src/client/components/EmptyFooter.vue +3 -0
  29. package/src/client/components/Footer.vue +87 -0
  30. package/src/client/components/Link.vue +31 -0
  31. package/src/client/components/LocaleSwitcher.vue +59 -0
  32. package/src/client/components/PostDate.vue +42 -0
  33. package/src/client/components/PostItem.vue +33 -0
  34. package/src/client/components/PostList.vue +23 -0
  35. package/src/client/components/ProjectCard.vue +68 -0
  36. package/src/client/components/ProjectList.vue +23 -0
  37. package/src/client/components/RouterLink.vue +19 -0
  38. package/src/client/components/StatusBadge.vue +56 -0
  39. package/src/client/components/TermBadge.vue +29 -0
  40. package/src/client/components/TermLink.vue +25 -0
  41. package/src/client/composables/date.ts +12 -0
  42. package/src/client/composables/index.ts +4 -0
  43. package/src/client/composables/locale.ts +7 -0
  44. package/src/client/composables/project.ts +30 -0
  45. package/src/client/composables/rank.ts +13 -0
  46. package/src/client/composables/slug.ts +12 -0
  47. package/src/client/index.ts +43 -0
  48. package/src/client/layouts/Layout.vue +25 -0
  49. package/src/client/pages/ArchivesPage.vue +43 -0
  50. package/src/client/pages/CategoriesPage.vue +30 -0
  51. package/src/client/pages/HomePage.vue +18 -0
  52. package/src/client/pages/Page.vue +20 -0
  53. package/src/client/pages/PostsPage.vue +23 -0
  54. package/src/client/pages/ProjectsPage.vue +65 -0
  55. package/src/client/pages/TagsPage.vue +30 -0
  56. package/src/client/plugins/donation/components/Donation.vue +43 -0
  57. package/src/client/plugins/donation/components/DonationButton.vue +22 -0
  58. package/src/client/plugins/donation/index.ts +42 -0
  59. package/src/client/plugins/google-analytics/components/GoogleAnalytics.vue +52 -0
  60. package/src/client/plugins/google-analytics/index.ts +8 -0
  61. package/src/client/plugins/i18n/index.ts +13 -0
  62. package/src/client/plugins/i18n/locales/en.json +25 -0
  63. package/src/client/plugins/i18n/locales/vi.json +25 -0
  64. package/src/client/styles/index.css +30 -0
  65. package/src/client/types/index.ts +50 -0
  66. package/src/node/composables/index.ts +3 -0
  67. package/src/node/composables/markdown.ts +14 -0
  68. package/src/node/composables/route.ts +26 -0
  69. package/src/node/composables/slug.ts +40 -0
  70. package/src/node/composables/types.ts +7 -0
  71. package/src/node/config.ts +79 -0
  72. 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="#">&#8203;</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,3 @@
1
+ <template>
2
+ <div />
3
+ </template>
@@ -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') }} &rarr;
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>