diff --git a/index.html b/index.html index 4826812..e78589a 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/public/_data/workshops.yml b/public/_data/workshops.yml index f394a77..c1a9977 100644 --- a/public/_data/workshops.yml +++ b/public/_data/workshops.yml @@ -12,9 +12,7 @@ duration: en: 3 × 1h 30min est: 3 × 1h 30min - date: - en: March 2026 - est: Märts 2026 + date: 2026-03 gallery: - temp1 - temp2 @@ -31,11 +29,8 @@ duration: en: 3 × 1h 30min est: 3 × 1h 30min - date: - en: March 2026 - est: Märts 2026 + date: 2026-03 gallery: - temp1 - temp2 - temp3 - diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 5a91d73..e3af82a 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -1,7 +1,9 @@ -import { onScopeDispose, ref, watch } from 'vue'; +import { computed, onScopeDispose, ref, watch } from 'vue'; import { getLanguageFromRoute } from '../routes'; const localeModules = import.meta.glob('./locales/*/*.json'); +const supportedLanguages = new Set(['est', 'en']); +export const languageLocales = { est: 'et-EE', en: 'en-GB' }; function getInitialLanguage() { if (typeof window === 'undefined') return 'est'; @@ -12,13 +14,17 @@ function getInitialLanguage() { return langFromRoute; } - return localStorage.getItem('language') || 'est'; + const savedLanguage = localStorage.getItem('language'); + return supportedLanguages.has(savedLanguage) ? savedLanguage : 'est'; } export const currentLang = ref(getInitialLanguage()); +export const currentLocale = computed(() => languageLocales[currentLang.value]); export const text = ref({}); +let translationRequest = 0; async function loadTranslations(lang) { + const request = ++translationRequest; try { const translations = {}; const matchingModules = Object.entries(localeModules).filter(([path]) => @@ -26,10 +32,10 @@ async function loadTranslations(lang) { ); const modules = await Promise.all(matchingModules.map(([, loader]) => loader())); modules.forEach((module) => Object.assign(translations, module.default || module)); - text.value = translations; + if (request === translationRequest) text.value = translations; } catch (error) { console.error(`Failed to load translations for ${lang}:`, error); - text.value = {}; + if (request === translationRequest) text.value = {}; } } @@ -37,14 +43,16 @@ watch(currentLang, loadTranslations, { immediate: true }); export function usePageText(pageName) { const pageText = ref({}); + let pageRequest = 0; const stop = watch(currentLang, async (lang) => { + const request = ++pageRequest; try { const loader = localeModules[`./locales/${lang}/${pageName}.json`]; const module = loader ? await loader() : null; - pageText.value = module?.default || module || {}; + if (request === pageRequest) pageText.value = module?.default || module || {}; } catch (error) { console.error(`Failed to load ${pageName} translations for ${lang}:`, error); - pageText.value = {}; + if (request === pageRequest) pageText.value = {}; } }, { immediate: true }); @@ -53,10 +61,15 @@ export function usePageText(pageName) { } export function switchLang(lang) { + if (!supportedLanguages.has(lang)) return; currentLang.value = lang; if (typeof window !== 'undefined') localStorage.setItem('language', lang); } +watch(currentLocale, (locale) => { + if (typeof document !== 'undefined') document.documentElement.lang = locale; +}, { immediate: true }); + if (typeof window !== 'undefined') { window.addEventListener('popstate', () => { const lang = getLanguageFromRoute(window.location.pathname); diff --git a/src/lib/index.js b/src/lib/index.js index bb08385..0cd4744 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1,5 +1,5 @@ export { navigate, goBack, reload, getPath, getQuery, switchLanguageRoute } from './router/router.js'; export { default as Router } from './router/Router.vue'; -export { currentLang, text, switchLang, usePageText } from './i18n.js'; +export { currentLang, currentLocale, languageLocales, text, switchLang, usePageText } from './i18n.js'; export { getLangText } from './langHelpers.js'; diff --git a/src/lib/locales/en/Helpdesk.json b/src/lib/locales/en/Helpdesk.json index 5c1ad23..081a1e6 100644 --- a/src/lib/locales/en/Helpdesk.json +++ b/src/lib/locales/en/Helpdesk.json @@ -23,16 +23,28 @@ "quantity": "Quantity/Time", "price": "Price" }, - "note": "Parts are ordered by the client themselves or by agreement with the technician.

If needed, we can come to the student village and Technopol area.

With our student-friendly prices and personal approach, we offer a service you won't be disappointed with!" + "notes": [ + "Parts are ordered by the client themselves or by agreement with the technician.", + "If needed, we can come to the student village and Technopol area.", + "With our student-friendly prices and personal approach, we offer a service you won't be disappointed with!" + ] }, "hours": { "title": "Opening Hours:", - "schedule": "Monday – Friday 14-19*", + "schedule": [ + { "emphasis": "M", "text": "onday – " }, + { "emphasis": "F", "text": "riday 14-19*" } + ], "note": "* This is the time period when we are most likely to be found. However, we cannot guarantee presence." }, "location": { "title": "Location", - "busInfo": "Accessible via buses 11, 37, 83, 91 from Keemia stop or buses 10, 27, 33, 36, 45 from Tehnikaülikool stop.", + "busInfo": [ + { "text": "Accessible via " }, + { "text": "buses 11, 37, 83, 91 from Keemia stop", "highlight": true }, + { "text": " or " }, + { "text": "buses 10, 27, 33, 36, 45 from Tehnikaülikool stop.", "highlight": true } + ], "address": "Akadeemia tee 5 dormitory, room 008A" } } diff --git a/src/lib/locales/en/Workshops.json b/src/lib/locales/en/Workshops.json index 9b080b2..412494e 100644 --- a/src/lib/locales/en/Workshops.json +++ b/src/lib/locales/en/Workshops.json @@ -2,7 +2,7 @@ "hero": { "label": "Our work", "title": "Workshops", - "titleHighlight": "& courses.", + "titleHighlight": "& courses.", "subtitle": "A selection of workshops conducted by our members over the years." }, "details": { diff --git a/src/lib/locales/est/Helpdesk.json b/src/lib/locales/est/Helpdesk.json index 164ff6b..7d5e8dc 100644 --- a/src/lib/locales/est/Helpdesk.json +++ b/src/lib/locales/est/Helpdesk.json @@ -23,16 +23,28 @@ "quantity": "Kogus/Aeg", "price": "Hind" }, - "note": "Jupid tellitakse kliendi enda poolt või kokkuleppel tehnikuga.

Vajadusel tuleme üliõpilasküla ja Tehnopoli piires kohale.

Oma tudengisõbralike hindade ja personaalse lähenemisega pakume teenust, milles ei pea pettuma!" + "notes": [ + "Jupid tellitakse kliendi enda poolt või kokkuleppel tehnikuga.", + "Vajadusel tuleme üliõpilasküla ja Tehnopoli piires kohale.", + "Oma tudengisõbralike hindade ja personaalse lähenemisega pakume teenust, milles ei pea pettuma!" + ] }, "hours": { "title": "Lahtiolekuajad:", - "schedule": "Esmaspäevast – Reedeni 14-19*", + "schedule": [ + { "emphasis": "E", "text": "smaspäevast – " }, + { "emphasis": "R", "text": "eedeni 14-19*" } + ], "note": "* Tegu on ajaperioodiga, mil meid on kõige tõenäolisem tabada. Siiski ei saa garanteerida kohalolekut." }, "location": { "title": "Asukoht", - "busInfo": "Saab 11, 37, 83, 91 bussiga Keemia peatusest või Tehnikaülikool peatusest 10, 27, 33, 36, 45 bussiga.", + "busInfo": [ + { "text": "Saab " }, + { "text": "11, 37, 83, 91 bussiga Keemia peatusest", "highlight": true }, + { "text": " või " }, + { "text": "Tehnikaülikool peatusest 10, 27, 33, 36, 45 bussiga.", "highlight": true } + ], "address": "Akadeemia tee 5 ühiselamu, ruum 008A" } } diff --git a/src/lib/locales/est/Workshops.json b/src/lib/locales/est/Workshops.json index 7f4dd9f..1e1b9f8 100644 --- a/src/lib/locales/est/Workshops.json +++ b/src/lib/locales/est/Workshops.json @@ -2,7 +2,7 @@ "hero": { "label": "Meie tehtud", "title": "Koolitused", - "titleHighlight": "& töötoad.", + "titleHighlight": "& töötoad.", "subtitle": "Valik koolitusi, mida meie liikmed on aastate jooksul läbi viinud." }, "details": { diff --git a/src/lib/router/Router.vue b/src/lib/router/Router.vue index 966f3e7..ec1bc7e 100644 --- a/src/lib/router/Router.vue +++ b/src/lib/router/Router.vue @@ -4,26 +4,29 @@ 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); +let routeRequest = 0; async function resolveCurrentComponent(path) { + const request = ++routeRequest; const routeEntry = props.routes[path] || props.routes['/']; if (typeof routeEntry !== 'function') { - CurrentComponent.value = routeEntry || null; + if (request === routeRequest) CurrentComponent.value = routeEntry || null; return; } try { const module = await routeEntry(); - CurrentComponent.value = module?.default || null; + if (request === routeRequest) CurrentComponent.value = module?.default || null; } catch { - CurrentComponent.value = null; + if (request === routeRequest) CurrentComponent.value = null; } } -function navigate(path) { - window.history.pushState({}, '', path); +function navigate(url) { + window.history.pushState({}, '', `${url.pathname}${url.search}${url.hash}`); window.scrollTo(0, 0); - currentPath.value = path; + currentPath.value = url.pathname; + window.dispatchEvent(new PopStateEvent('popstate')); } const handlePopState = () => { @@ -31,10 +34,16 @@ const handlePopState = () => { }; const handleClick = (event) => { - if (event.target.tagName === 'A' && event.target.getAttribute('href')?.startsWith('/')) { - event.preventDefault(); - navigate(event.target.getAttribute('href')); - } + if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null; + if (!anchor || anchor.target || anchor.hasAttribute('download')) return; + + const url = new URL(anchor.href, window.location.href); + if (url.origin !== window.location.origin || !props.routes[url.pathname]) return; + if (url.pathname === window.location.pathname && url.hash) return; + + event.preventDefault(); + navigate(url); }; onMounted(() => { diff --git a/src/lib/router/router.js b/src/lib/router/router.js index a4dc64f..209da11 100644 --- a/src/lib/router/router.js +++ b/src/lib/router/router.js @@ -9,29 +9,34 @@ import { switchLang } from '../i18n.js'; */ export function navigate(path, options = {}) { const { external = false } = options; - + if (external) { window.open(path, '_blank', 'noopener,noreferrer'); } else { - const selectedLang = - localStorage.getItem('language') || - getLanguageFromRoute(window.location.pathname) || - 'est'; - - const resolvedPath = getTranslatedRoute(path, selectedLang); - - window.history.pushState({}, "", resolvedPath); - window.scrollTo(0, 0); - window.dispatchEvent(new PopStateEvent('popstate')); - - // Update language based on new route - const lang = getLanguageFromRoute(resolvedPath); - if (lang) { - switchLang(lang); + const url = new URL(path, window.location.origin); + if (url.origin !== window.location.origin) { + window.location.assign(url.href); + return; } + + const selectedLang = + getLanguageFromRoute(window.location.pathname) || + localStorage.getItem('language') || + 'est'; + url.pathname = getTranslatedRoute(url.pathname, selectedLang); + commitNavigation(`${url.pathname}${url.search}${url.hash}`); } } +function commitNavigation(path) { + window.history.pushState({}, '', path); + window.scrollTo(0, 0); + window.dispatchEvent(new PopStateEvent('popstate')); + + const lang = getLanguageFromRoute(new URL(path, window.location.origin).pathname); + if (lang) switchLang(lang); +} + /** * Switch language and navigate to translated route * @param {string} targetLang - Target language ('est' or 'en') @@ -40,12 +45,10 @@ export function switchLanguageRoute(targetLang) { const currentPath = window.location.pathname; const translatedPath = getTranslatedRoute(currentPath, targetLang); - // Always update the language first switchLang(targetLang); - - // Only navigate if the path is different + if (translatedPath !== currentPath) { - navigate(translatedPath); + commitNavigation(translatedPath); } } @@ -78,4 +81,4 @@ export function getPath() { export function getQuery() { const params = new URLSearchParams(window.location.search); return Object.fromEntries(params.entries()); -} \ No newline at end of file +} diff --git a/src/lib/yaml.js b/src/lib/yaml.js new file mode 100644 index 0000000..dab8867 --- /dev/null +++ b/src/lib/yaml.js @@ -0,0 +1,18 @@ +import yaml from 'js-yaml'; + +export async function loadYaml(path, fallback) { + try { + const response = await fetch(path); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const value = yaml.load(await response.text()); + const matchesFallback = Array.isArray(fallback) + ? Array.isArray(value) + : typeof fallback === 'object' && fallback !== null + ? typeof value === 'object' && value !== null && !Array.isArray(value) + : value !== undefined && value !== null; + return matchesFallback ? value : fallback; + } catch (error) { + console.warn(`Failed to load YAML from ${path}:`, error); + return fallback; + } +} diff --git a/src/routes/Calendar.vue b/src/routes/Calendar.vue index 0b678dd..34887d2 100644 --- a/src/routes/Calendar.vue +++ b/src/routes/Calendar.vue @@ -1,10 +1,23 @@