feat: setup unplugin-vue-router, fixed eslnt config, SmartLink for type-safe internal/external links

This commit is contained in:
Benjamin Singleton 2026-02-12 01:18:14 -06:00
parent 3c0e46c9ab
commit 60ce6d75f7
15 changed files with 435 additions and 311 deletions

View file

@ -1,24 +1,44 @@
import js from '@eslint/js';
import globals from 'globals';
import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import js from "@eslint/js";
import globals from "globals";
import ts from "typescript-eslint";
import vue from "eslint-plugin-vue";
import { defineConfig, globalIgnores } from "eslint/config";
import vueParser from "vue-eslint-parser";
export default ts.config(
{ ignores: ['dist'] },
{
extends: [
js.configs.recommended,
...ts.configs.recommendedTypeChecked,
...vue.configs.recommendedTypeChecked,
],
files: ['**/*.{js,ts,vue}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
)
export default defineConfig([
globalIgnores(["dist"]),
{
extends: [
js.configs.recommended,
ts.configs.strictTypeChecked,
...vue.configs["flat/essential"],
],
files: ["./src/**/*.{js,ts,vue}"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: globals.browser,
parser: vueParser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
parser: ts.parser,
extraFileExtensions: [".vue"],
},
},
rules: {
"vue/no-restricted-component-names": [
"error",
{
name: "RouterLink",
message: "Use SmartLink instead of RouterLink.",
},
],
},
},
// disable multi-word-component-names for unplugin-vue-router
{
files: ["src/pages/**/*.vue"],
rules: { "vue/multi-word-component-names": "off" },
},
]);

View file

@ -22,17 +22,20 @@
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@repo/common": "workspace:*",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.26.0",
"eslint-plugin-vue": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-vue": "^10.7.0",
"globals": "^17.3.0",
"prettier": "^3.5.3",
"sass": "^1.87.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1",
"typescript-eslint": "^8.55.0",
"unplugin-vue-router": "^0.12.0",
"vite": "^6.3.5",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^2.2.8"
},
"packageManager": "pnpm@10.11.0"

View file

@ -4,6 +4,8 @@ import { ref, type Ref } from "vue";
import LocalePicker from "./components/organisms/LocalePicker.vue";
import { vOnClickOutside } from "@vueuse/components";
import { useLocale } from "./i18n";
import { RouterLink, useRouter } from "vue-router";
import SmartLink from "./components/organisms/SmartLink.vue";
const burgerOpen: Ref<boolean> = ref<boolean>(false);
@ -16,6 +18,11 @@ const closeBurger = (): void => {
};
const locale = useLocale();
const router = useRouter();
router.beforeEach(() => {
closeBurger();
});
</script>
<template>
@ -44,21 +51,15 @@ const locale = useLocale();
<div :class="`navbar-menu ${burgerOpen ? 'is-active' : ''}`">
<div class="navbar-start">
<RouterLink
class="navbar-item"
to="/"
@click="closeBurger()"
>{{ locale.navbar.whatIsViossa }}</RouterLink
>
<RouterLink
class="navbar-item"
to="/resources"
@click="closeBurger()"
>{{ locale.navbar.resources }}</RouterLink
>
<RouterLink class="navbar-item" to="/kotoba">
<SmartLink class="navbar-item" internal to="/">{{
locale.navbar.whatIsViossa
}}</SmartLink>
<SmartLink class="navbar-item" internal to="/resources">{{
locale.navbar.resources
}}</SmartLink>
<SmartLink class="navbar-item" internal to="/kotoba">
{{ locale.navbar.kotoba }}
</RouterLink>
</SmartLink>
<LocalePicker class="navbar-item" />
</div>
</div>

View file

@ -1,53 +1,54 @@
<template>
<div class="box columns is-vcentered is-gap-4">
<div class="column is-one-quarter" v-if="image">
<figure class="image">
<img :src="image" :alt="alt" :title="alt" />
</figure>
</div>
<div class="column">
<h4 class="title">{{ title }}</h4>
<p class="subtitle">{{ subtitle }}</p>
<p class="content">{{ desc }}</p>
<div class="box columns is-vcentered is-gap-4">
<div class="column is-one-quarter" v-if="image">
<figure class="image">
<img :src="image" :alt="alt" :title="alt" />
</figure>
</div>
<div class="column">
<h4 class="title">{{ title }}</h4>
<p class="subtitle">{{ subtitle }}</p>
<p class="content">{{ desc }}</p>
<div class="level">
<a :href="link"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-primary is-medium"
:text="joinText">
</a>
<div class="level">
<a
:href="link"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-primary is-medium"
>{{ joinText }}</a
>
<a :href="``"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-info is-outlined is-medium"
:text="rulesText">
</a>
<a
:href="``"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-info is-outlined is-medium"
>{{ rulesText }}</a
>
<a :href="rulesLink"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-warning is-outlined is-medium"
:text="rulesText">
</a>
</div>
</div>
</div>
<a
:href="rulesLink"
target="_blank"
rel="noopener noreferrer nofollow"
class="button is-warning is-outlined is-medium"
>{{ rulesText }}</a
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
title: String,
subtitle: String,
desc: String,
link: String,
rulesLink: String,
joinText: String,
rulesText: String,
image: { type: String, required: false },
alt: { type: String, required: false },
})
</script>
title: String,
subtitle: String,
desc: String,
link: String,
rulesLink: String,
joinText: String,
rulesText: String,
image: { type: String, required: false },
alt: { type: String, required: false },
});
</script>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { RouterLinkProps } from "vue-router";
import type { RouteNamedMap } from "vue-router/auto-routes";
defineProps<
Omit<RouterLinkProps, "to">
& (
| { internal: true; to: keyof RouteNamedMap }
| { external: true; to: `https://${string}` | `http://${string}` }
)
>();
</script>
<template>
<RouterLink v-bind="$props">
<slot />
</RouterLink>
</template>

View file

@ -1,5 +1,5 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./routes";
import router from "./router";
createApp(App).use(router).mount("#app");

View file

@ -1,10 +1,9 @@
<script setup lang="ts">
import { useLocale } from '@/i18n';
import { useLocale } from "@/i18n";
const locale = useLocale();
</script>
<template>
<div>
<section class="section">
@ -17,14 +16,10 @@ const locale = useLocale();
</div>
<div class="block is-flex is-flex-direction-row is-gap-2">
<input class="input" type="text"></input>
</div>
<div class="container">
<input class="input" type="text" />
</div>
<div class="container"></div>
</section>
</div>
</template>

View file

@ -0,0 +1,10 @@
import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";
const router = createRouter({ history: createWebHistory(), routes });
if (import.meta.hot) {
handleHotUpdate(router);
}
export default router;

View file

@ -1,21 +0,0 @@
import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
import HomePage from "@/components/pages/HomePage.vue";
import ResourcesPage from "@/components/pages/ResourcesPage.vue";
import KotobaPage from "@/components/pages/KotobaPage.vue";
const routes: RouteRecordRaw[] = [
{ path: "/", name: "Home", component: HomePage },
{ path: "/resources", name: "Resources", component: ResourcesPage },
{ path: "/kotoba", name: "Kotoba", component: KotobaPage },
// {
// path: '/:pathMatch(.*)*', // Vue Router 4 catch-all for 404s
// name: 'NotFound',
// component: NotFoundPage,
// },
];
const router = createRouter({ history: createWebHistory(), routes });
export default router;

View file

@ -18,5 +18,8 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/kotoba': RouteRecordInfo<'/kotoba', '/kotoba', Record<never, never>, Record<never, never>>,
'/resources': RouteRecordInfo<'/resources', '/resources', Record<never, never>, Record<never, never>>,
}
}

View file

@ -12,14 +12,20 @@
"noUncheckedIndexedAccess": true,
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"rootDir": "src",
"paths": {
"@/*": [
"src/*"
"./src/*"
]
},
},
"vueCompilerOptions": {
"strictTemplates": true,
},
"include": [
"src"
"src",
],
"exclude": [
"./eslint.config.js"
]
}

View file

@ -1,9 +1,10 @@
import path from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueRouter from "unplugin-vue-router/vite";
export default defineConfig({
plugins: [vue({})],
plugins: [vueRouter({ root: "src", routesFolder: "pages" }), vue({})],
resolve: { alias: { "@": path.resolve(import.meta.dirname, "src") } },
server: { port: 1224 },
});