mirror of
https://github.com/Lapikud/lapikud.github.io.git
synced 2026-08-09 03:59:13 +00:00
refactor(vue): migrate app, routes, and tooling from Svelte
Replace the Svelte runtime, Vite plugin, Storybook adapter, and component files with Vue 3 equivalents while preserving the existing route table, page markup, styling, assets, deployment shape, and npm workflows. Move shared and page-level state onto Vue refs, computed values, lifecycle hooks, and script setup components. Recreate the custom async router and translation composable as Vue-native modules so navigation and bilingual content continue to work without introducing a router or state-management dependency. Adopt the maintained @lucide/vue package, remove the Svelte icon and map wrappers, and initialize MapLibre directly on the Helpdesk page. Replace the Svelte CSF story with a Vue Storybook story, remove obsolete aliases and generation/config files, and update the audited lockfile with no known vulnerabilities.
This commit is contained in:
@@ -1,103 +1,65 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { onScopeDispose, ref, watch } from 'vue';
|
||||
import { getLanguageFromRoute } from '../routes';
|
||||
|
||||
const localeModules = import.meta.glob('./locales/*/*.json');
|
||||
|
||||
// Get initial language from URL or localStorage
|
||||
function getInitialLanguage() {
|
||||
if (typeof window === 'undefined') return 'est';
|
||||
|
||||
const path = window.location.pathname;
|
||||
const langFromRoute = getLanguageFromRoute(path);
|
||||
|
||||
// If we detected a language from route, use it and save it
|
||||
|
||||
const langFromRoute = getLanguageFromRoute(window.location.pathname);
|
||||
if (langFromRoute) {
|
||||
localStorage.setItem('language', langFromRoute);
|
||||
return langFromRoute;
|
||||
}
|
||||
|
||||
// Otherwise use saved language or default to 'est'
|
||||
|
||||
return localStorage.getItem('language') || 'est';
|
||||
}
|
||||
|
||||
const initialLang = getInitialLanguage();
|
||||
|
||||
export const currentLang = writable(initialLang);
|
||||
|
||||
// Store for translations
|
||||
export const text = writable({});
|
||||
export const currentLang = ref(getInitialLanguage());
|
||||
export const text = ref({});
|
||||
|
||||
async function loadTranslations(lang) {
|
||||
try {
|
||||
const translations = {};
|
||||
|
||||
// Dynamically import all translation files for the specific language
|
||||
// Vite will code-split these and only bundle the ones actually used
|
||||
for (const path in localeModules) {
|
||||
if (path.includes(`/locales/${lang}/`) || path.includes(`\\locales\\${lang}\\`)) {
|
||||
const module = await localeModules[path]();
|
||||
Object.assign(translations, module.default || module);
|
||||
}
|
||||
}
|
||||
|
||||
text.set(translations);
|
||||
const matchingModules = Object.entries(localeModules).filter(([path]) =>
|
||||
path.includes(`/locales/${lang}/`),
|
||||
);
|
||||
const modules = await Promise.all(matchingModules.map(([, loader]) => loader()));
|
||||
modules.forEach((module) => Object.assign(translations, module.default || module));
|
||||
text.value = translations;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load translations for ${lang}:`, error);
|
||||
text.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
loadTranslations(initialLang);
|
||||
watch(currentLang, loadTranslations, { immediate: true });
|
||||
|
||||
/**
|
||||
* Create a translation store that only loads one page file per language.
|
||||
* Example: createPageTextStore('Home') -> ./locales/est/Home.json or ./locales/en/Home.json
|
||||
*/
|
||||
export function createPageTextStore(pageName) {
|
||||
const pageText = writable({});
|
||||
|
||||
async function loadPageTranslations(lang) {
|
||||
export function usePageText(pageName) {
|
||||
const pageText = ref({});
|
||||
const stop = watch(currentLang, async (lang) => {
|
||||
try {
|
||||
const filePath = `./locales/${lang}/${pageName}.json`;
|
||||
const loader = localeModules[filePath];
|
||||
|
||||
if (!loader) {
|
||||
pageText.set({});
|
||||
return;
|
||||
}
|
||||
|
||||
const module = await loader();
|
||||
pageText.set(module.default || module);
|
||||
const loader = localeModules[`./locales/${lang}/${pageName}.json`];
|
||||
const module = loader ? await loader() : null;
|
||||
pageText.value = module?.default || module || {};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${pageName} translations for ${lang}:`, error);
|
||||
pageText.set({});
|
||||
pageText.value = {};
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const unsubscribe = currentLang.subscribe((lang) => {
|
||||
loadPageTranslations(lang);
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe: pageText.subscribe,
|
||||
destroy: unsubscribe,
|
||||
};
|
||||
onScopeDispose(stop);
|
||||
return pageText;
|
||||
}
|
||||
|
||||
export function switchLang(lang) {
|
||||
currentLang.set(lang);
|
||||
loadTranslations(lang);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('language', lang);
|
||||
}
|
||||
currentLang.value = lang;
|
||||
if (typeof window !== 'undefined') localStorage.setItem('language', lang);
|
||||
}
|
||||
|
||||
// Update language when route changes
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('popstate', () => {
|
||||
const path = window.location.pathname;
|
||||
const lang = getLanguageFromRoute(path);
|
||||
if (lang) {
|
||||
switchLang(lang);
|
||||
}
|
||||
const lang = getLanguageFromRoute(window.location.pathname);
|
||||
if (lang) switchLang(lang);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { navigate, goBack, reload, getPath, getQuery, switchLanguageRoute } from './router/router.js';
|
||||
export { default as Router } from './router/Router.svelte';
|
||||
export { default as Router } from './router/Router.vue';
|
||||
|
||||
export { currentLang, text, switchLang, createPageTextStore } from './i18n.js';
|
||||
export { getLangText } from './langHelpers.js';
|
||||
export { currentLang, text, switchLang, usePageText } from './i18n.js';
|
||||
export { getLangText } from './langHelpers.js';
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<!-- DO NOT TOUCH THIS FILE AT ANY COST -->
|
||||
<!-- unless the router is broken 👉👈 -->
|
||||
|
||||
<script>
|
||||
export function routeTo(path) {
|
||||
window.history.pushState({}, "", path);
|
||||
window.scrollTo(0, 0);
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
}
|
||||
|
||||
let { routes } = $props();
|
||||
|
||||
let currentPath = $state(window.location.pathname);
|
||||
let CurrentComponent = $state(null);
|
||||
|
||||
async function resolveCurrentComponent(path) {
|
||||
const routeEntry = routes[path] || routes["/"];
|
||||
|
||||
if (typeof routeEntry === "function") {
|
||||
try {
|
||||
const module = await routeEntry();
|
||||
CurrentComponent = module?.default || null;
|
||||
} catch {
|
||||
CurrentComponent = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentComponent = routeEntry || null;
|
||||
}
|
||||
|
||||
function navigate(path) {
|
||||
window.history.pushState({}, "", path);
|
||||
window.scrollTo(0, 0);
|
||||
currentPath = path;
|
||||
}
|
||||
|
||||
// Handle back/forward buttons
|
||||
$effect(() => {
|
||||
const handlePopState = () => {
|
||||
currentPath = window.location.pathname;
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
return () => window.removeEventListener("popstate", handlePopState);
|
||||
});
|
||||
|
||||
// Intercept link clicks
|
||||
$effect(() => {
|
||||
const handleClick = (e) => {
|
||||
if (
|
||||
e.target.tagName === "A" &&
|
||||
e.target.getAttribute("href")?.startsWith("/")
|
||||
) {
|
||||
e.preventDefault();
|
||||
navigate(e.target.getAttribute("href"));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("click", handleClick);
|
||||
|
||||
return () => window.removeEventListener("click", handleClick);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
resolveCurrentComponent(currentPath);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if CurrentComponent}
|
||||
<CurrentComponent />
|
||||
{/if}
|
||||
53
src/lib/router/Router.vue
Normal file
53
src/lib/router/Router.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, shallowRef, watch } from 'vue';
|
||||
|
||||
const props = defineProps({ routes: { type: Object, required: true } });
|
||||
const currentPath = shallowRef(window.location.pathname);
|
||||
const CurrentComponent = shallowRef(null);
|
||||
|
||||
async function resolveCurrentComponent(path) {
|
||||
const routeEntry = props.routes[path] || props.routes['/'];
|
||||
if (typeof routeEntry !== 'function') {
|
||||
CurrentComponent.value = routeEntry || null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await routeEntry();
|
||||
CurrentComponent.value = module?.default || null;
|
||||
} catch {
|
||||
CurrentComponent.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function navigate(path) {
|
||||
window.history.pushState({}, '', path);
|
||||
window.scrollTo(0, 0);
|
||||
currentPath.value = path;
|
||||
}
|
||||
|
||||
const handlePopState = () => {
|
||||
currentPath.value = window.location.pathname;
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (event.target.tagName === 'A' && event.target.getAttribute('href')?.startsWith('/')) {
|
||||
event.preventDefault();
|
||||
navigate(event.target.getAttribute('href'));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
window.addEventListener('click', handleClick);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
window.removeEventListener('click', handleClick);
|
||||
});
|
||||
watch(currentPath, resolveCurrentComponent, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="CurrentComponent" v-if="CurrentComponent" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user