fixed language issues, updated routing, completed first version of managment page

This commit is contained in:
Henri
2026-04-07 23:55:38 +03:00
parent ae1a44b65f
commit 3d27217769
11 changed files with 208 additions and 558 deletions

View File

@@ -11,6 +11,7 @@
// Icon imports (Lucide) // Icon imports (Lucide)
import Coffee from "lucide-svelte/icons/coffee"; import Coffee from "lucide-svelte/icons/coffee";
import Presentation from "lucide-svelte/icons/presentation"; import Presentation from "lucide-svelte/icons/presentation";
import Gamepad2 from "lucide-svelte/icons/gamepad-2";
import Lectern from "lucide-svelte/icons/lectern"; import Lectern from "lucide-svelte/icons/lectern";
import Bot from "lucide-svelte/icons/bot"; import Bot from "lucide-svelte/icons/bot";
import HandHeart from "lucide-svelte/icons/hand-heart"; import HandHeart from "lucide-svelte/icons/hand-heart";
@@ -33,7 +34,7 @@
"/management", "/management",
]; ];
const eventsRoutes = ["/koolitused"]; const eventsRoutes = ["/koolitused", "/workshops"];
function isCurrent(path) { function isCurrent(path) {
return currentPath === path; return currentPath === path;
@@ -268,11 +269,17 @@
panelClass={dropdownPanelClass()} panelClass={dropdownPanelClass()}
> >
<Button <Button
class={dropdownItemClass(isCurrent("/koolitused"))} class={dropdownItemClass(isCurrent("/koolitused") || isCurrent("/workshops"))}
onClick={() => handleNavigation("/koolitused")} onClick={() => handleNavigation("/koolitused")}
> >
<Presentation />{$text.eventsPages.workshops} <Presentation />{$text.eventsPages.workshops}
</Button> </Button>
<Button
class={dropdownItemClass(false)}
onClick={() => handleNavigation("https://tipilan.ee/", { external: true })}
>
<Gamepad2 />{$text.eventsPages.tipilan}
</Button>
<Button <Button
class={dropdownItemClass(false)} class={dropdownItemClass(false)}
onClick={() => handleNavigation("https://asikarikas.ee/", { external: true })} onClick={() => handleNavigation("https://asikarikas.ee/", { external: true })}
@@ -398,6 +405,13 @@
<Presentation size={20} /> <Presentation size={20} />
{$text.eventsPages.workshops} {$text.eventsPages.workshops}
</Button> </Button>
<Button
onClick={() => handleNavigation("https://tipilan.ee/", { external: true })}
class="text-left transition-colors flex items-center gap-2 bg-transparent hover:text-orange-500 border-transparent"
>
<Gamepad2 size={20} />
{$text.eventsPages.tipilan}
</Button>
<Button <Button
onClick={() => handleNavigation("https://asikarikas.ee/", { external: true })} onClick={() => handleNavigation("https://asikarikas.ee/", { external: true })}
class="text-left transition-colors flex items-center gap-2 bg-transparent hover:text-orange-500 border-transparent" class="text-left transition-colors flex items-center gap-2 bg-transparent hover:text-orange-500 border-transparent"

View File

@@ -1,6 +1,8 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { getLanguageFromRoute } from '../routes'; import { getLanguageFromRoute } from '../routes';
const localeModules = import.meta.glob('./locales/*/*.json');
// Get initial language from URL or localStorage // Get initial language from URL or localStorage
function getInitialLanguage() { function getInitialLanguage() {
if (typeof window === 'undefined') return 'est'; if (typeof window === 'undefined') return 'est';
@@ -31,11 +33,9 @@ async function loadTranslations(lang) {
// Dynamically import all translation files for the specific language // Dynamically import all translation files for the specific language
// Vite will code-split these and only bundle the ones actually used // Vite will code-split these and only bundle the ones actually used
const modules = import.meta.glob('./locales/*/*.json'); for (const path in localeModules) {
for (const path in modules) {
if (path.includes(`/locales/${lang}/`) || path.includes(`\\locales\\${lang}\\`)) { if (path.includes(`/locales/${lang}/`) || path.includes(`\\locales\\${lang}\\`)) {
const module = await modules[path](); const module = await localeModules[path]();
Object.assign(translations, module.default || module); Object.assign(translations, module.default || module);
} }
} }
@@ -48,6 +48,41 @@ async function loadTranslations(lang) {
loadTranslations(initialLang); loadTranslations(initialLang);
/**
* 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) {
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);
} catch (error) {
console.error(`Failed to load ${pageName} translations for ${lang}:`, error);
pageText.set({});
}
}
const unsubscribe = currentLang.subscribe((lang) => {
loadPageTranslations(lang);
});
return {
subscribe: pageText.subscribe,
destroy: unsubscribe,
};
}
export function switchLang(lang) { export function switchLang(lang) {
currentLang.set(lang); currentLang.set(lang);
loadTranslations(lang); loadTranslations(lang);

View File

@@ -1,5 +1,5 @@
export { navigate, goBack, reload, getPath, getQuery, switchLanguageRoute } from './router/router.js'; 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.svelte';
export { currentLang, text, switchLang } from './i18n.js'; export { currentLang, text, switchLang, createPageTextStore } from './i18n.js';
export { getLangText } from './langHelpers.js'; export { getLangText } from './langHelpers.js';

View File

@@ -5,8 +5,7 @@
"intro": "People who keep the organisation moving forward." "intro": "People who keep the organisation moving forward."
}, },
"current": { "current": {
"label": "Current board", "label": "Current board"
"hint": "Contacts and responsibilities"
}, },
"history": { "history": {
"label": "People who have led the organisation over time", "label": "People who have led the organisation over time",

View File

@@ -8,11 +8,11 @@
}, },
"events": "Events", "events": "Events",
"eventsPages": { "eventsPages": {
"calendar": "Events Calendar",
"workshops": "Workshops", "workshops": "Workshops",
"fresh": "Freshmen Initiation", "tipilan": "TipiLAN",
"repair": "Repair Café" "asi": "ASI Karikas",
"repair": "Remondikohvik"
}, },
"contact": "Contact", "helpdesk": "Helpdesk",
"helpdesk": "Helpdesk" "contact": "Contact"
} }

View File

@@ -5,8 +5,7 @@
"intro": "Inimesed, kes viivad organisatsiooni edasi." "intro": "Inimesed, kes viivad organisatsiooni edasi."
}, },
"current": { "current": {
"label": "Praegune juhtkond", "label": "Praegune juhtkond"
"hint": "Kontaktid ja vastutusvaldkonnad"
}, },
"history": { "history": {
"label": "Organisatsiooni juhtinud inimesed läbi aegade", "label": "Organisatsiooni juhtinud inimesed läbi aegade",

View File

@@ -8,11 +8,11 @@
}, },
"events": "Üritused", "events": "Üritused",
"eventsPages": { "eventsPages": {
"calendar": "Ürituste Kalender",
"workshops": "Koolitused", "workshops": "Koolitused",
"fresh": "Rebaste Ristimine", "tipilan": "TipiLAN",
"asi": "ASI Karikas",
"repair": "Remondikohvik" "repair": "Remondikohvik"
}, },
"contact": "Kontakt", "helpdesk": "Helpdesk",
"helpdesk": "Helpdesk" "contact": "Kontakt"
} }

View File

@@ -13,11 +13,18 @@ export function navigate(path, options = {}) {
if (external) { if (external) {
window.open(path, '_blank', 'noopener,noreferrer'); window.open(path, '_blank', 'noopener,noreferrer');
} else { } else {
window.history.pushState({}, "", path); const selectedLang =
localStorage.getItem('language') ||
getLanguageFromRoute(window.location.pathname) ||
'est';
const resolvedPath = getTranslatedRoute(path, selectedLang);
window.history.pushState({}, "", resolvedPath);
window.dispatchEvent(new PopStateEvent('popstate')); window.dispatchEvent(new PopStateEvent('popstate'));
// Update language based on new route // Update language based on new route
const lang = getLanguageFromRoute(path); const lang = getLanguageFromRoute(resolvedPath);
if (lang) { if (lang) {
switchLang(lang); switchLang(lang);
} }

View File

@@ -1,8 +1,8 @@
<script> <script>
import {Section, Stack, Grid, Center, Image, Svg, Button} from "$components"; import {Section, Stack, Grid, Center, Image, Svg, Button} from "$components";
import { navigate } from "$lib/router/router.js"; import { navigate } from "$lib/router/router.js";
import { text, currentLang } from "$lib"; import { createPageTextStore, currentLang } from "$lib";
import { onMount } from "svelte"; import { onMount, onDestroy } from "svelte";
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import ArrowRight from "lucide-svelte/icons/arrow-right"; import ArrowRight from "lucide-svelte/icons/arrow-right";
@@ -10,18 +10,17 @@
// Partners data // Partners data
let partners = []; let partners = [];
let textData = {}; const text = createPageTextStore("Home");
// Subscribe to text store updates
text.subscribe(value => {
textData = value;
});
onMount(async () => { onMount(async () => {
const response = await fetch('/_data/partners.yml'); const response = await fetch('/_data/partners.yml');
const yamlText = await response.text(); const yamlText = await response.text();
partners = yaml.load(yamlText); partners = yaml.load(yamlText);
}); });
onDestroy(() => {
text.destroy();
});
</script> </script>
<!-- Hero Section --> <!-- Hero Section -->

View File

@@ -1,8 +1,15 @@
<script> <script>
import { Section, Grid, Container, Image } from "$components"; import {
import { text } from "$lib"; Section,
Grid,
Container,
Image,
} from "$components";
import { onMount } from "svelte"; import { onMount } from "svelte";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { currentLang, getLangText } from "$lib";
import estManagementText from "$lib/locales/est/Management.json";
import enManagementText from "$lib/locales/en/Management.json";
import Mail from "lucide-svelte/icons/mail"; import Mail from "lucide-svelte/icons/mail";
import Phone from "lucide-svelte/icons/phone"; import Phone from "lucide-svelte/icons/phone";
@@ -11,53 +18,19 @@
let loading = true; let loading = true;
let loadError = false; let loadError = false;
const fallbackText = { const managementTextByLang = {
est: { est: estManagementText,
hero: { en: enManagementText,
eyebrow: "MTÜ Lapikud",
title: "Juhtkond",
intro: "Inimesed, kes viivad organisatsiooni edasi.",
},
current: {
label: "Praegune juhtkond",
hint: "Kontaktid ja vastutusvaldkonnad",
},
history: {
label: "Organisatsiooni juhtinud inimesed läbi aegade",
hint: "Tänu kõigile, kes on Lapikuid juhtinud",
},
loading: "Laadin andmeid…",
error: "Juhtkonna andmete laadimine ebaõnnestus.",
},
en: {
hero: {
eyebrow: "MTÜ Lapikud",
title: "Management",
intro: "People who keep the organisation moving forward.",
},
current: {
label: "Current board",
hint: "Contacts and responsibilities",
},
history: {
label: "People who have led the organisation over time",
hint: "Thanks to everyone who has helped guide Lapikud",
},
loading: "Loading data…",
error: "Failed to load the management data.",
},
}; };
function getText() { $: t = managementTextByLang[$currentLang] || managementTextByLang.est;
return $text.management ?? fallbackText.est;
}
function parseYearLabel(label) { function parseYearLabel(label) {
const text = String(label ?? ""); const value = String(label ?? "");
const match = text.match(/^(\d{4})(?:\s*\((.+)\))?$/); const match = value.match(/^(\d{4})(?:\s*\((.+)\))?$/);
if (!match) { if (!match) {
return { year: text, tag: "" }; return { year: value, tag: "" };
} }
return { return {
@@ -66,44 +39,28 @@
}; };
} }
function getBoardLabel(member) {
if (!member) return "";
if (member.role && member.role !== "Juhatuse liige") {
return member.role;
}
return member.subrole || member.role || "";
}
function getPhoneHref(phone) {
if (!phone) return "";
return `tel:${String(phone).replace(/[\s()-]/g, "")}`;
}
function getInitials(name) { function getInitials(name) {
return String(name ?? "") return (
.split(/\s+/) String(name ?? "")
.filter(Boolean) .split(/\s+/)
.slice(0, 2) .filter(Boolean)
.map((part) => part[0]?.toUpperCase() ?? "") .slice(0, 2)
.join("") || "?"; .map((part) => part[0]?.toUpperCase() ?? "")
.join("") || "?"
);
} }
onMount(async () => { onMount(async () => {
try { try {
const [currentRes, pastRes] = await Promise.all([ const currentRes = await fetch('/_data/management.yml');
fetch("/_data/management.yml"), const currentYaml = await currentRes.text();
fetch("/_data/past_management.yml"),
]);
const [currentYaml, pastYaml] = await Promise.all([
currentRes.text(),
pastRes.text(),
]);
currentManagement = yaml.load(currentYaml) || []; currentManagement = yaml.load(currentYaml) || [];
const pastRes = await fetch('/_data/past_management.yml');
const pastYaml = await pastRes.text();
pastManagement = yaml.load(pastYaml) || []; pastManagement = yaml.load(pastYaml) || [];
} catch (error) { } catch (error) {
console.error("Error loading management data:", error); console.error('Error loading management data:', error);
loadError = true; loadError = true;
} finally { } finally {
loading = false; loading = false;
@@ -116,137 +73,101 @@
padding="none" padding="none"
fullWidth={true} fullWidth={true}
contentClass="!px-0 !py-0" contentClass="!px-0 !py-0"
class="management-page overflow-hidden text-white" class="overflow-hidden text-white"
background="#0d0d0d"
> >
<div class="hero-shell"> <div class="relative overflow-hidden bg-linear-to-b bg-gray-900 after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-[rgba(240,148,29,0.16)] after:content-['']">
<div class="hero-orb hero-orb-1" aria-hidden="true"></div> <div class="pointer-events-none absolute -right-16 -top-16 h-80 w-80 rounded-full border border-[rgba(240,148,29,0.12)]" aria-hidden="true"></div>
<div class="hero-orb hero-orb-2" aria-hidden="true"></div> <div class="pointer-events-none absolute -bottom-20 right-16 h-52 w-52 rounded-full border border-[rgba(240,148,29,0.08)]" aria-hidden="true"></div>
<Container class="relative z-10 py-[clamp(3rem,8vw,6rem)]"> <Container class="relative z-10 py-[clamp(3rem,8vw,6rem)]">
<div class="max-w-4xl"> <div class="max-w-4xl">
<p class="eyebrow">{getText().hero.eyebrow}</p> <p class="mb-4 text-xs tracking-[0.16em] uppercase text-orange-500">{t.hero.eyebrow}</p>
<h1 class="hero-title">{getText().hero.title}</h1> <h1 class="m-0 text-[clamp(2.4rem,6vw,4.2rem)] font-bold">{t.hero.title}</h1>
<p class="hero-intro">{getText().hero.intro}</p> <p class="mt-4 max-w-[52ch] text-[clamp(1.05rem,1.5vw,1.25rem)] text-white/75">{t.hero.intro}</p>
</div> </div>
</Container> </Container>
</div> </div>
</Section> </Section>
<Section padding="large" class="current-section bg-white text-gray-900"> <!-- Current Management Section -->
<Container> <Section padding="large">
<div class="section-label"> <h1 class="m-0 text-[clamp(1.8rem,3vw,2.6rem)] leading-[1.12] tracking-[-0.03em] mb-10">{t.current.label}</h1>
<span>{getText().current.label}</span> <Grid min="500px">
<span class="section-label-line"></span> {#each currentManagement as member (member.name)}
</div> <div class="flex flex-col sm:flex-row gap-6 items-start">
<div class="w-full sm:w-auto sm:max-w-[200px] aspect-square shrink-0">
<div class="section-heading-row"> <img
<div> src={`/assets/management-images/${member.photo}`}
<h2 class="section-title">{getText().current.label}</h2> alt={member.name}
<p class="section-subtitle">{getText().current.hint}</p> class="w-full h-full object-cover rounded-md"
</div> />
</div> </div>
<div class="flex flex-col gap-2 grow">
{#if loading} <h3 class="text-2xl font-medium">{member.name}</h3>
<p class="section-message">{getText().loading}</p> <p class="text-base text-gray-700">
{:else if loadError} {getLangText(member, 'role', $currentLang)} - {getLangText(member, 'subrole', $currentLang)}
<p class="section-message section-message-error">{getText().error}</p> </p>
{:else} <a
<Grid min="280px" gap="gap-6" class="board-grid"> href="mailto:{member.email}"
{#each currentManagement as member (member.name)} class="text-base flex items-center gap-2 text-orange-500 hover:opacity-80 transition"
<article class="board-card"> >
<div class="board-card-media"> <Mail size={18} />{member.email}
{#if member.photo} </a>
<Image <a
src={`/assets/management-images/${member.photo}`} href="tel:{member.phone}"
alt={member.name} class="text-base flex items-center gap-2 text-orange-500 hover:opacity-80 transition"
objectFit="cover" >
class="board-card-image" <Phone size={18} />{member.phone}
pictureClass="board-card-picture" </a>
/> </div>
{:else} </div>
<div class="board-card-placeholder">{getInitials(member.name)}</div> {/each}
{/if} </Grid>
</div>
<div class="board-card-body">
<div class="board-card-domain">{getBoardLabel(member)}</div>
<div class="board-card-name">{member.name}</div>
<div class="board-card-contacts">
{#if member.email}
<a href={`mailto:${member.email}`}>
<Mail size={14} strokeWidth={2} />
<span>{member.email}</span>
</a>
{/if}
{#if member.phone}
<a href={getPhoneHref(member.phone)}>
<Phone size={14} strokeWidth={2} />
<span>{member.phone}</span>
</a>
{/if}
</div>
</div>
</article>
{/each}
</Grid>
{/if}
</Container>
</Section> </Section>
<div class="divider"></div> <!-- Past Management Section -->
<Section padding="large">
<Section padding="large" class="history-section" background="#fffdfa"> <div class="mb-7 flex flex-col items-start justify-between gap-6 md:flex-row md:items-end">
<Container>
<div class="section-label">
<span>{getText().history.label}</span>
<span class="section-label-line"></span>
</div>
<div class="section-heading-row">
<div> <div>
<h2 class="section-title">{getText().history.label}</h2> <h2 class="m-0 text-[clamp(1.8rem,3vw,2.6rem)] leading-[1.12] tracking-[-0.03em]">{t.history.label}</h2>
<p class="section-subtitle">{getText().history.hint}</p> <p class="mt-2 text-[0.98rem] text-[rgba(13,13,13,0.62)]">{t.history.hint}</p>
</div> </div>
</div> </div>
{#if loading} {#if loading}
<p class="section-message">{getText().loading}</p> <p class="m-0 py-4 text-black">{t.loading}</p>
{:else if loadError} {:else if loadError}
<p class="section-message section-message-error">{getText().error}</p> <p class="m-0 py-4 text-red-500">{t.error}</p>
{:else} {:else}
<div class="history-list"> <div class="flex flex-col gap-10">
{#each pastManagement as yearData (yearData.year)} {#each pastManagement as yearData (yearData.year)}
<section class="year-block"> <section class="mb-5">
<div class="year-header"> <div class="mb-4 flex w-full flex-col items-start gap-3 border-b border-orange-500 pb-3">
<div class="year-heading-group"> <div class="flex items-baseline gap-3 whitespace-nowrap">
<span class="year-number">{parseYearLabel(yearData.year).year}</span> <span class="text-[1.6rem] leading-none font-bold text-orange-500 tracking-[-0.04em]">{parseYearLabel(yearData.year).year}</span>
{#if parseYearLabel(yearData.year).tag} {#if parseYearLabel(yearData.year).tag}
<span class="year-tag">{parseYearLabel(yearData.year).tag}</span> <span class="rounded-full px-[0.55rem] py-[0.2rem] text-[0.64rem] tracking-widest uppercase self-center text-orange-500">{parseYearLabel(yearData.year).tag}</span>
{/if} {/if}
</div> </div>
<span class="year-line"></span>
</div> </div>
<Grid min="150px" gap="gap-5" class="year-people"> <Grid min="150px" mobileColumns={2} gap="gap-5" class="items-start">
{#each yearData.members as member (member.name)} {#each yearData.members as member (member.name)}
<div class="person-item"> <div class="group flex flex-col items-center gap-2.5 text-center">
<div class="person-photo"> <div class="h-24 w-24 overflow-hidden rounded-[10px] bg-linear-to-br outline-2 outline-transparent outline-offset-2 transition [@media(min-width:769px)]:h-28 [@media(min-width:769px)]:w-28">
{#if member.image} {#if member.image}
<Image <Image
src={`/assets/past-management-images/optimised/${member.image}`} src={`/assets/past-management-images/optimised/${member.image}`}
alt={member.name} alt={member.name}
objectFit="cover" objectFit="cover"
class="person-image" class="block h-full w-full"
pictureClass="person-picture" pictureClass="h-full w-full"
/> />
{:else} {:else}
<div class="person-placeholder">{getInitials(member.name)}</div> <div class="grid h-full w-full place-items-center text-sm font-bold text-orange-600">{getInitials(member.name)}</div>
{/if} {/if}
</div> </div>
<span class="person-name">{member.name}</span> <span class="max-w-[14ch] text-[0.86rem] font-semibold leading-[1.35] text-gray-900">{member.name}</span>
</div> </div>
{/each} {/each}
</Grid> </Grid>
@@ -254,359 +175,5 @@
{/each} {/each}
</div> </div>
{/if} {/if}
</Container>
</Section> </Section>
</div> </div>
<style>
.hero-shell {
position: relative;
overflow: hidden;
background: linear-gradient(180deg, #0d0d0d 0%, #111111 100%);
}
.hero-shell::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 1px;
background: rgba(240, 148, 29, 0.16);
}
.hero-orb {
position: absolute;
border: 1px solid rgba(240, 148, 29, 0.12);
border-radius: 999px;
pointer-events: none;
}
.hero-orb-1 {
top: -4rem;
right: -4rem;
width: 20rem;
height: 20rem;
}
.hero-orb-2 {
bottom: -5rem;
right: 4rem;
width: 13rem;
height: 13rem;
border-color: rgba(240, 148, 29, 0.08);
}
.eyebrow {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.75rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #f0941d;
margin: 0 0 1rem;
}
.hero-title {
margin: 0;
font-size: clamp(2.4rem, 6vw, 4.2rem);
line-height: 1.05;
font-weight: 700;
letter-spacing: -0.04em;
}
.hero-intro {
margin: 1rem 0 0;
max-width: 52ch;
font-size: clamp(1.05rem, 1.5vw, 1.25rem);
line-height: 1.7;
color: rgba(255, 255, 255, 0.76);
}
.section-label {
display: flex;
align-items: center;
gap: 0.8rem;
margin-bottom: 2rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #f0941d;
}
.section-label-line {
flex: 1;
height: 1px;
background: rgba(13, 13, 13, 0.12);
}
.section-heading-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1.5rem;
margin-bottom: 1.75rem;
}
.section-title {
margin: 0;
font-size: clamp(1.8rem, 3vw, 2.6rem);
line-height: 1.12;
letter-spacing: -0.03em;
}
.section-subtitle {
margin: 0.45rem 0 0;
color: rgba(13, 13, 13, 0.62);
font-size: 0.98rem;
}
.section-message {
margin: 0;
padding: 1rem 0;
color: rgba(13, 13, 13, 0.65);
}
.section-message-error {
color: #b42318;
}
:global(.board-grid) {
align-items: stretch;
}
.board-card {
overflow: hidden;
border-radius: 14px;
border: 1px solid rgba(13, 13, 13, 0.08);
background: #ffffff;
transition: transform 0.2s ease, border-color 0.2s ease,
box-shadow 0.2s ease;
box-shadow: 0 1px 0 rgba(13, 13, 13, 0.02);
}
.board-card:hover {
transform: translateY(-4px);
border-color: rgba(240, 148, 29, 0.35);
box-shadow: 0 12px 30px rgba(13, 13, 13, 0.08);
}
.board-card-media {
aspect-ratio: 4 / 3;
background: linear-gradient(135deg, #f7f1e6, #efe5d3);
overflow: hidden;
}
:global(.board-card-picture),
:global(.board-card-image) {
width: 100%;
height: 100%;
}
:global(.board-card-image) {
display: block;
}
.board-card-placeholder {
width: 100%;
height: 100%;
display: grid;
place-items: center;
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.04em;
color: #d67d0a;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
.board-card-body {
padding: 1.2rem 1.2rem 1.25rem;
border-top: 1px solid rgba(13, 13, 13, 0.06);
}
.board-card-domain {
display: inline-flex;
align-items: center;
margin-bottom: 0.7rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(240, 148, 29, 0.1);
color: #d67d0a;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.65rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.board-card-name {
margin: 0 0 0.8rem;
font-size: 1.1rem;
font-weight: 650;
line-height: 1.25;
}
.board-card-contacts {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.board-card-contacts a {
display: inline-flex;
align-items: center;
gap: 0.45rem;
color: rgba(13, 13, 13, 0.54);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.77rem;
font-weight: 500;
text-decoration: none;
transition: color 0.2s ease;
}
.board-card-contacts a:hover {
color: #f0941d;
text-decoration: none;
}
.divider {
height: 1px;
background: rgba(13, 13, 13, 0.08);
}
:global(.history-section) {
background: #fffdfa;
}
.history-list {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.year-block {
margin: 0;
}
.year-header {
display: flex;
align-items: baseline;
gap: 1rem;
margin-bottom: 1.1rem;
}
.year-heading-group {
display: flex;
align-items: baseline;
gap: 0.75rem;
white-space: nowrap;
}
.year-number {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1;
}
.year-tag {
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(240, 148, 29, 0.12);
color: #d67d0a;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.64rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.year-line {
flex: 1;
height: 1px;
background: rgba(13, 13, 13, 0.1);
}
:global(.year-people) {
align-items: start;
}
.person-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.55rem;
text-align: center;
}
.person-photo {
width: 112px;
height: 112px;
overflow: hidden;
border-radius: 10px;
background: linear-gradient(135deg, #efe6d7, #e7dbc7);
outline: 2px solid transparent;
outline-offset: 2px;
transition: outline-color 0.2s ease, transform 0.2s ease;
}
.person-item:hover .person-photo {
outline-color: #f0941d;
transform: translateY(-1px);
}
:global(.person-picture),
:global(.person-image) {
width: 100%;
height: 100%;
}
:global(.person-image) {
display: block;
}
.person-placeholder {
width: 100%;
height: 100%;
display: grid;
place-items: center;
color: #d67d0a;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.9rem;
font-weight: 700;
}
.person-name {
max-width: 14ch;
font-size: 0.86rem;
line-height: 1.35;
font-weight: 600;
color: #0d0d0d;
}
@media (max-width: 768px) {
.section-heading-row {
align-items: flex-start;
flex-direction: column;
}
.year-header {
align-items: flex-start;
flex-direction: column;
}
.year-line {
width: 100%;
}
.person-photo {
width: 96px;
height: 96px;
}
}
</style>

View File

@@ -43,18 +43,45 @@ export const routeMap = {
// Juhatus/Managment // Juhatus/Managment
'/juhatus': { est: '/juhatus', en: '/management' }, '/juhatus': { est: '/juhatus', en: '/management' },
'/management': { est: '/juhatus', en: '/management' }, '/management': { est: '/juhatus', en: '/management' },
// Workshops/Koolitused
'/koolitused': { est: '/koolitused', en: '/workshops' },
'/workshops': { est: '/koolitused', en: '/workshops' },
// Calendar/Kalender
'/kalender': { est: '/kalender', en: '/calendar' },
'/calendar': { est: '/kalender', en: '/calendar' },
}; };
// Detect language from route // Detect language from route
export function getLanguageFromRoute(path) { export function getLanguageFromRoute(path) {
const estRoutes = ['/tudengile', '/mentorid', '/ettevottele', '/lapikutest', '/kontakt']; const estRoutes = [
const enRoutes = ['/student', '/mentors', '/ourwork', '/aboutus', '/contact']; '/tudengile',
'/mentorid',
'/ettevottele',
'/lapikutest',
'/kontakt',
'/juhatus',
'/koolitused',
'/kalender',
];
const enRoutes = [
'/student',
'/mentors',
'/ourwork',
'/aboutus',
'/contact',
'/management',
'/workshops',
'/calendar',
];
if (estRoutes.includes(path)) return 'est'; if (estRoutes.includes(path)) return 'est';
if (enRoutes.includes(path)) return 'en'; if (enRoutes.includes(path)) return 'en';
// Default to Estonian // Neutral/shared route (e.g. '/', '/helpdesk')
return 'est'; // -> keep currently selected language
return null;
} }
// Get translated route // Get translated route
@@ -79,6 +106,9 @@ export const routes = {
'/contact': Contact, '/contact': Contact,
'/juhatus': Managment, '/juhatus': Managment,
'/management': Managment, '/management': Managment,
'/koolitused': Home, // TODO
'/workshops': Home, // TODO
'/striim': Striim, '/striim': Striim,
'/kalender': Calendar, '/kalender': Calendar,
'/calendar': Calendar,
} }