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:
topsinoty
2026-07-04 15:51:11 +03:00
parent 33eac3b896
commit 13322e11cb
55 changed files with 3684 additions and 2456 deletions

View File

@@ -1,21 +0,0 @@
<script>
import {
Section,
} from "$components";
import { onDestroy } from "svelte";
import { createPageTextStore } from "$lib";
const text = createPageTextStore("AboutUs");
onDestroy(() => {
text.destroy();
});
</script>
<div class="safe-area-navbar">
<Section>
<p>In development...</p>
<p>Põhikiri siia?</p>
</Section>
</div>

19
src/routes/AboutUs.vue Normal file
View File

@@ -0,0 +1,19 @@
<script setup>
import {
Section,
} from "../components/index.js";
import { usePageText } from "../lib/index.js";
const text = usePageText("AboutUs");
</script>
<template>
<div class="safe-area-navbar">
<Section>
<p>In development...</p>
<p>Põhikiri siia?</p>
</Section>
</div>
</template>

View File

@@ -1,34 +0,0 @@
<script>
import { Section, Center } from "$components";
import { onMount, onDestroy } from "svelte";
import { createPageTextStore } from "$lib";
let sectionElement;
const text = createPageTextStore("Calendar");
onMount(() => {
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
onDestroy(() => {
text.destroy();
});
</script>
<div class="safe-area-navbar" bind:this={sectionElement}>
<Section >
<Center dir="col" class="w-full">
<div class="w-full max-w-4xl">
<iframe
style="border: 0;"
src="https://www.google.com/calendar/embed?src=lapikud%40gmail.com&ctz=Europe/Tallinn&showTitle=0&showPrint=0&showCalendars=0&showTz=0&wkst=2"
title={$text.iframeTitle || "Lapikud Calendar"}
class="w-full"
height="600"
frameborder="0"
scrolling="no"
></iframe>
</div>
</Center>
</Section>
</div>

32
src/routes/Calendar.vue Normal file
View File

@@ -0,0 +1,32 @@
<script setup>
import { Section, Center } from "../components/index.js";
import { onMounted, ref } from 'vue';
import { usePageText } from "../lib/index.js";
const sectionElement = ref(null);
const text = usePageText("Calendar");
onMounted(() => {
sectionElement.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
</script>
<template>
<div class="safe-area-navbar" ref="sectionElement">
<Section >
<Center dir="col" class="w-full">
<div class="w-full max-w-4xl">
<iframe
style="border: 0;"
src="https://www.google.com/calendar/embed?src=lapikud%40gmail.com&ctz=Europe/Tallinn&showTitle=0&showPrint=0&showCalendars=0&showTz=0&wkst=2"
:title="text.iframeTitle || 'Lapikud Calendar'"
class="w-full"
height="600"
frameborder="0"
scrolling="no"
></iframe>
</div>
</Center>
</Section>
</div>
</template>

View File

@@ -1,21 +0,0 @@
<script>
import {
Section,
Grid,
Center,
} from "$components";
import { onDestroy } from "svelte";
import { createPageTextStore } from "$lib";
const text = createPageTextStore("Contact");
onDestroy(() => {
text.destroy();
});
</script>
<div class="safe-area-navbar">
<Section>
<p>In development...</p>
</Section>
</div>

19
src/routes/Contact.vue Normal file
View File

@@ -0,0 +1,19 @@
<script setup>
import {
Section,
Grid,
Center,
} from "../components/index.js";
import { usePageText } from "../lib/index.js";
const text = usePageText("Contact");
</script>
<template>
<div class="safe-area-navbar">
<Section>
<p>In development...</p>
</Section>
</div>
</template>

View File

@@ -1,205 +0,0 @@
<script>
import {
Section,
Grid,
} from "$components";
import { onMount, onDestroy } from "svelte";
import { createPageTextStore } from "$lib";
import { getHeroImageFallback, getHeroImageSrcSet, getRootAssetPath } from "$lib/imageHelpers.js";
import yaml from 'js-yaml';
import { MapLibre, Marker } from "svelte-maplibre";
import MapPin from "lucide-svelte/icons/map-pin";
import BusFront from "lucide-svelte/icons/bus-front";
import { navigate } from "$lib/router/router.js";
// Images
const helpdeskbg = getHeroImageFallback("helpdesk-page-images", "helpdesk_bg");
const helpdeskbgWebpSet = getHeroImageSrcSet("helpdesk-page-images", "helpdesk_bg");
const helpdesk = getRootAssetPath("helpdesk-page-images", "helpdesk-on-black.png");
// Coordinates for Akadeemia tee 5, 12616 Tallinn [longitude, latitude]
const center = [24.66887400515207, 59.396427975093935];
// Coordinates for Keemia bus stop [longitude, latitude]
const keemiaBusStop = [24.668325396145953, 59.397074517662276];
// Coordinates for Tehnikaülikool bus stop [longitude, latitude]
const tehnikaBusStop = [24.67328945396854, 59.39508874042947];
// Google Maps directions URL
const getDirectionsUrl = (lng, lat, loc) => {
if (loc == "bus"){
return `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&travelmode=transit`;
}
return `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
};
// Navigate to directions using router
const openDirections = (lng, lat, loc) => {
navigate(getDirectionsUrl(lng, lat, loc), { external: true });
};
// Load pricing data
let pricingData = { services: [] };
const text = createPageTextStore("Helpdesk");
onMount(async () => {
const response = await fetch('/_data/hinnakiri.yml');
const yamlText = await response.text();
pricingData = yaml.load(yamlText);
});
onDestroy(() => {
text.destroy();
});
</script>
<div class="safe-area-navbar">
<!-- Hero Section -->
<Section class="relative bg-gray-900 min-h-[42vh] overflow-hidden" padding="none" fullWidth={true}>
<div class="absolute inset-0 left-1/2 w-screen -translate-x-1/2 h-[42vh]" aria-hidden="true">
<picture>
<source srcset={helpdeskbgWebpSet} type="image/webp" />
<img
class="h-full w-full object-cover object-left opacity-50 blur-[1.5px]"
src={helpdeskbg}
alt="Helpdesk"
/>
</picture>
</div>
<div class="relative flex justify-center items-center flex-row w-full h-[42vh] z-10">
<img src={helpdesk} alt={$text["hero"]?.title || "Helpdesk logo"} />
</div>
</Section>
<!-- Main Content -->
<Section>
<Grid columns={2} tabletColumns={1} mobileColumns={1} gap="gap-6">
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">{$text["whatIs"]?.title || ""}</h2>
<p class="text-lg">
{$text["whatIs"]?.description || ""}
</p>
</div>
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">{$text["services"]?.title || ""}</h2>
<ul class="text-lg list-disc pl-5">
{#each $text["services"]?.list || [] as service}
<li>{service}</li>
{/each}
</ul>
</div>
<!-- Pricing Table -->
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">{$text["pricing"]?.title || ""}</h2>
<div class="overflow-hidden rounded-lg border border-gray-200 shadow-sm">
<table class="w-full text-lg">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-4 text-left font-semibold text-gray-900 border-b border-gray-200">
{$text["pricing"]?.tableHeaders?.service || ""}
</th>
<th class="px-3 py-4 text-left font-semibold text-gray-900 border-b border-gray-200">
{$text["pricing"]?.tableHeaders?.quantity || ""}
</th>
<th class="px-3 py-4 text-left font-semibold text-gray-900 border-b border-gray-200">
{$text["pricing"]?.tableHeaders?.price || ""}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each pricingData.services as service}
<tr class="hover:bg-gray-50 transition-colors duration-200">
<td class="px-3 py-4 text-gray-900">{service.name}</td>
<td class="px-3 py-4 text-gray-600">{service.quantity}</td>
<td class="px-3 py-4 font-medium" class:text-green-600={service.price === "Tasuta"} class:text-gray-900={service.price !== "Tasuta"}>
{service.price}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<p class="pt-4 text-lg font-bold">
{@html $text["pricing"]?.note || ""}
</p>
</div>
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">
{@html $text["hours"]?.title || ""}
<br />
{@html $text["hours"]?.schedule || ""}
</h2>
<p class="text-lg font-light opacity-70 pb-16">
{$text["hours"]?.note || ""}
</p>
<h2 class="text-4xl font-light pb-4">{$text["location"]?.title || ""}</h2>
<div class="w-full h-96 rounded-lg overflow-hidden">
<MapLibre
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
class="relative w-full h-full"
{center}
zoom={15}
interactive={false}
attributionControl={false}
>
<!-- Main location -->
<Marker lngLat={center}>
<div
role="button"
tabindex="0"
onclick={() => openDirections(center[0], center[1])}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && openDirections(center[0], center[1])}
class="flex flex-col items-center hover:scale-110 transition-transform cursor-pointer"
title="Get directions to Akadeemia tee 5"
>
<span class="text-xs font-semibold text-white bg-orange-500 px-2 py-0.5 rounded shadow-md mb-1 whitespace-nowrap">Akadeemia tee 5</span>
<MapPin class="w-7 h-7 text-orange-500 drop-shadow-lg" />
</div>
</Marker>
<!-- Keemia bus stop -->
<Marker lngLat={keemiaBusStop}>
<div
role="button"
tabindex="0"
onclick={() => openDirections(keemiaBusStop[0], keemiaBusStop[1], "bus")}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && openDirections(keemiaBusStop[0], keemiaBusStop[1], "bus")}
class="flex flex-col items-center hover:scale-110 transition-transform cursor-pointer"
title="Get directions to Keemia bus stop"
>
<span class="text-xs font-semibold text-gray-900 bg-white px-2 py-0.5 rounded shadow-md mb-1">Keemia</span>
<BusFront class="w-7 h-7 text-orange-500 drop-shadow-lg" />
</div>
</Marker>
<!-- Tehnikaülikool bus stop -->
<Marker lngLat={tehnikaBusStop}>
<div
role="button"
tabindex="0"
onclick={() => openDirections(tehnikaBusStop[0], tehnikaBusStop[1], "bus")}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && openDirections(tehnikaBusStop[0], tehnikaBusStop[1], "bus")}
class="flex flex-col items-center hover:scale-110 transition-transform cursor-pointer"
title="Get directions to Tehnikaülikool bus stop"
>
<span class="text-xs font-semibold text-gray-900 bg-white px-2 py-0.5 rounded shadow-md mb-1">Tehnikaülikool</span>
<BusFront class="w-7 h-7 text-orange-500 drop-shadow-lg" />
</div>
</Marker>
</MapLibre>
</div>
<p class="pb-4 text-lg">
{@html $text["location"]?.busInfo || ""}
</p>
<p class="text-lg font-bold">
{$text["location"]?.address || ""}
</p>
</div>
</Grid>
</Section>
</div>

202
src/routes/Helpdesk.vue Normal file
View File

@@ -0,0 +1,202 @@
<script setup>
import {
Section,
Grid,
} from "../components/index.js";
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { usePageText } from "../lib/index.js";
import { getHeroImageFallback, getHeroImageSrcSet, getRootAssetPath } from "../lib/imageHelpers.js";
import yaml from 'js-yaml';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { navigate } from "../lib/router/router.js";
// Images
const helpdeskbg = getHeroImageFallback("helpdesk-page-images", "helpdesk_bg");
const helpdeskbgWebpSet = getHeroImageSrcSet("helpdesk-page-images", "helpdesk_bg");
const helpdesk = getRootAssetPath("helpdesk-page-images", "helpdesk-on-black.png");
// Coordinates for Akadeemia tee 5, 12616 Tallinn [longitude, latitude]
const center = [24.66887400515207, 59.396427975093935];
// Coordinates for Keemia bus stop [longitude, latitude]
const keemiaBusStop = [24.668325396145953, 59.397074517662276];
// Coordinates for Tehnikaülikool bus stop [longitude, latitude]
const tehnikaBusStop = [24.67328945396854, 59.39508874042947];
// Google Maps directions URL
const getDirectionsUrl = (lng, lat, loc) => {
if (loc == "bus"){
return `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&travelmode=transit`;
}
return `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
};
// Navigate to directions using router
const openDirections = (lng, lat, loc) => {
navigate(getDirectionsUrl(lng, lat, loc), { external: true });
};
// Load pricing data
const pricingData = ref({ services: [] });
const mapElement = ref(null);
let map;
const text = usePageText("Helpdesk");
onMounted(async () => {
const response = await fetch('/_data/hinnakiri.yml');
const yamlText = await response.text();
pricingData.value = yaml.load(yamlText) || { services: [] };
map = new maplibregl.Map({
container: mapElement.value,
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
center,
zoom: 15,
interactive: false,
attributionControl: false,
});
const locations = [
{ coordinates: center, label: 'Akadeemia tee 5', type: 'location' },
{ coordinates: keemiaBusStop, label: 'Keemia', type: 'bus' },
{ coordinates: tehnikaBusStop, label: 'Tehnikaülikool', type: 'bus' },
];
locations.forEach(({ coordinates, label, type }) => {
const marker = document.createElement('button');
marker.type = 'button';
marker.className = `helpdesk-marker helpdesk-marker--${type}`;
marker.setAttribute('aria-label', `Get directions to ${label}`);
marker.innerHTML = `<span>${label}</span><b aria-hidden="true">${type === 'bus' ? '●' : '◆'}</b>`;
marker.addEventListener('click', () => openDirections(coordinates[0], coordinates[1], type));
new maplibregl.Marker({ element: marker }).setLngLat(coordinates).addTo(map);
});
});
onBeforeUnmount(() => map?.remove());
</script>
<template>
<div class="safe-area-navbar">
<!-- Hero Section -->
<Section class="relative bg-gray-900 min-h-[42vh] overflow-hidden" padding="none" :fullWidth="true">
<div class="absolute inset-0 left-1/2 w-screen -translate-x-1/2 h-[42vh]" aria-hidden="true">
<picture>
<source :srcset="helpdeskbgWebpSet" type="image/webp" />
<img
class="h-full w-full object-cover object-left opacity-50 blur-[1.5px]"
:src="helpdeskbg"
alt="Helpdesk"
/>
</picture>
</div>
<div class="relative flex justify-center items-center flex-row w-full h-[42vh] z-10">
<img :src="helpdesk" :alt="text['hero']?.title || 'Helpdesk logo'" />
</div>
</Section>
<!-- Main Content -->
<Section>
<Grid :columns="2" :tabletColumns="1" :mobileColumns="1" gap="gap-6">
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">{{ text.whatIs?.title || '' }}</h2>
<p class="text-lg">
{{ text.whatIs?.description || '' }}
</p>
</div>
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">{{ text.services?.title || '' }}</h2>
<ul class="text-lg list-disc pl-5">
<template v-for="service in text.services?.list || []">
<li>{{ service }}</li>
</template>
</ul>
</div>
<!-- Pricing Table -->
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">{{ text.pricing?.title || '' }}</h2>
<div class="overflow-hidden rounded-lg border border-gray-200 shadow-sm">
<table class="w-full text-lg">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-4 text-left font-semibold text-gray-900 border-b border-gray-200">
{{ text.pricing?.tableHeaders?.service || '' }}
</th>
<th class="px-3 py-4 text-left font-semibold text-gray-900 border-b border-gray-200">
{{ text.pricing?.tableHeaders?.quantity || '' }}
</th>
<th class="px-3 py-4 text-left font-semibold text-gray-900 border-b border-gray-200">
{{ text.pricing?.tableHeaders?.price || '' }}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<template v-for="service in pricingData.services">
<tr class="hover:bg-gray-50 transition-colors duration-200">
<td class="px-3 py-4 text-gray-900">{{ service.name }}</td>
<td class="px-3 py-4 text-gray-600">{{ service.quantity }}</td>
<td class="px-3 py-4 font-medium" :class="service.price === 'Tasuta' ? 'text-green-600' : 'text-gray-900'">
{{ service.price }}
</td>
</tr>
</template>
</tbody>
</table>
</div>
<p class="pt-4 text-lg font-bold">
<span v-html="text.pricing?.note || ''"></span>
</p>
</div>
<div class="flex flex-col">
<h2 class="text-4xl font-light pb-4">
<span v-html="text.hours?.title || ''"></span>
<br />
<span v-html="text.hours?.schedule || ''"></span>
</h2>
<p class="text-lg font-light opacity-70 pb-16">
{{ text.hours?.note || '' }}
</p>
<h2 class="text-4xl font-light pb-4">{{ text.location?.title || '' }}</h2>
<div ref="mapElement" class="relative w-full h-96 rounded-lg overflow-hidden"></div>
<p class="pb-4 text-lg">
<span v-html="text.location?.busInfo || ''"></span>
</p>
<p class="text-lg font-bold">
{{ text.location?.address || '' }}
</p>
</div>
</Grid>
</Section>
</div>
</template>
<style>
.helpdesk-marker {
display: flex;
cursor: pointer;
flex-direction: column;
align-items: center;
border: 0;
background: transparent;
transition: transform 150ms ease;
}
.helpdesk-marker:hover { transform: scale(1.1); }
.helpdesk-marker span {
margin-bottom: 0.25rem;
border-radius: 0.25rem;
background: white;
padding: 0.125rem 0.5rem;
color: #111827;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
box-shadow: 0 2px 5px rgb(0 0 0 / 20%);
}
.helpdesk-marker--location span { background: var(--orange); color: white; }
.helpdesk-marker b { color: var(--orange); font-size: 1.5rem; line-height: 1; }
</style>

View File

@@ -1,26 +1,22 @@
<script>
import {Section, Stack, Grid, Center, Image, Svg, Button} from "$components";
import { navigate } from "$lib/router/router.js";
import { createPageTextStore, currentLang } from "$lib";
import { getHeroImagePath, getHeroImageSrcSet, getHeroImageFallback, getOptimisedImagePath, getOptimisedImageFallback } from "$lib/imageHelpers.js";
import { onMount, onDestroy } from "svelte";
<script setup>
import {Section, Stack, Grid, Center, Image, Svg, Button} from "../components/index.js";
import { navigate } from "../lib/router/router.js";
import { usePageText, currentLang } from "../lib/index.js";
import { getHeroImagePath, getHeroImageSrcSet, getHeroImageFallback, getOptimisedImagePath, getOptimisedImageFallback } from "../lib/imageHelpers.js";
import { onMounted, ref } from 'vue';
import yaml from 'js-yaml';
import ArrowRight from "lucide-svelte/icons/arrow-right";
import ArrowLeft from "lucide-svelte/icons/arrow-left";
import { ArrowRight } from "@lucide/vue";
import { ArrowLeft } from "@lucide/vue";
// Partners data
let partners = [];
const text = createPageTextStore("Home");
const partners = ref([]);
const text = usePageText("Home");
onMount(async () => {
onMounted(async () => {
const response = await fetch('/_data/partners.yml');
const yamlText = await response.text();
partners = yaml.load(yamlText);
});
onDestroy(() => {
text.destroy();
partners.value = yaml.load(yamlText) || [];
});
const partnerLogoExtByName = {
@@ -37,15 +33,16 @@
}
</script>
<template>
<!-- Hero Section -->
<Section class="relative min-h-screen overflow-hidden" padding="none" bg="bg-gray-900" id="hero">
<div class="relative min-h-screen flex items-center">
<div class="absolute inset-0 left-1/2 w-screen -translate-x-1/2" aria-hidden="true">
<Image
webpSrc={getHeroImagePath("home-page-images", "hero")}
src={getHeroImageFallback("home-page-images", "hero")}
webpSrcSet={getHeroImageSrcSet("home-page-images", "hero")}
alt={$text["hero"]?.imageAlt || "illustrative hero image"}
:webpSrc="getHeroImagePath('home-page-images', 'hero')"
:src="getHeroImageFallback('home-page-images', 'hero')"
:webpSrcSet="getHeroImageSrcSet('home-page-images', 'hero')"
:alt="text['hero']?.imageAlt || 'illustrative hero image'"
objectFit="cover"
class="w-full h-full"
pictureClass="block w-full h-full"
@@ -73,13 +70,13 @@
<div class="flex w-full max-w-115 flex-col justify-end">
<div class="flex flex-col items-start gap-5">
<p class="w-full max-w-110 text-left text-lg leading-[165%] text-white drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)] md:max-w-125 md:text-xl">
{$text["hero"]?.description || "MTÜ Lapikud on Tallinna Tehnikaülikooli Tarkvaraarendusklubi, mis ühendab endisi ja praegusi IT huvilisi tudengeid."}
{{ text.hero?.description || "MTÜ Lapikud on Tallinna Tehnikaülikooli Tarkvaraarendusklubi, mis ühendab endisi ja praegusi IT huvilisi tudengeid." }}
</p>
<Button
class="rounded-lg border-orange-500 bg-orange-500 px-3 py-2 text-gray-900 transition-colors hover:border-orange-300 hover:bg-orange-300"
onClick={() => navigate("/tudengile")}
:onClick="() => navigate('/tudengile')"
>
{$text["hero"]?.joinButton || "Liitu LAPikutega!"}
{{ text.hero?.joinButton || "Liitu LAPikutega!" }}
</Button>
</div>
</div>
@@ -100,50 +97,50 @@
/>
</div>
<Grid columns={2} largeColumns={3} mobileColumns={1} class="relative z-10 items-start mt-12 md:mt-20" gap="gap-12 md:gap-16 lg:gap-24">
<Grid :columns="2" :largeColumns="3" :mobileColumns="1" class="relative z-10 items-start mt-12 md:mt-20" gap="gap-12 md:gap-16 lg:gap-24">
<Center dir="col" class="w-full max-w-sm justify-start mx-auto">
<div class="w-[min(72vw,240px)] md:w-[min(26vw,280px)] lg:w-[min(23vw,300px)] aspect-square rounded-full overflow-hidden">
<Image
webpSrc={getOptimisedImagePath("home-page-images", "student_temp", "webp")}
src={getOptimisedImageFallback("home-page-images", "student_temp")}
alt={$text["services"]?.student?.imageAlt || "Tudengile Pilt"}
:webpSrc="getOptimisedImagePath('home-page-images', 'student_temp', 'webp')"
:src="getOptimisedImageFallback('home-page-images', 'student_temp')"
:alt="text['services']?.student?.imageAlt || 'Tudengile Pilt'"
objectFit="cover"
class="w-full h-full rounded-full object-[center_35%]"
pictureClass="block w-full h-full"
/>
</div>
<h3 class="w-full min-h-10 text-center text-2xl font-semibold flex items-center justify-center">{$text["services"]?.student?.title || "Tudengile"}</h3>
<p class="w-full max-w-[26ch] text-center text-base">{$text["services"]?.student?.description || "Tule arenda oma oskuseid ja saa ägedaid sõpru! Omanda praktilist kogemust reaalsetest projektidest."}</p>
<h3 class="w-full min-h-10 text-center text-2xl font-semibold flex items-center justify-center">{{ text.services?.student?.title || "Tudengile" }}</h3>
<p class="w-full max-w-[26ch] text-center text-base">{{ text.services?.student?.description || "Tule arenda oma oskuseid ja saa ägedaid sõpru! Omanda praktilist kogemust reaalsetest projektidest." }}</p>
</Center>
<Center dir="col" class="w-full max-w-sm justify-start mx-auto">
<div class="w-[min(72vw,240px)] md:w-[min(26vw,280px)] lg:w-[min(23vw,300px)] aspect-square rounded-full overflow-hidden">
<Image
webpSrc={getOptimisedImagePath("home-page-images", "helpdesk", "webp")}
src={getOptimisedImageFallback("home-page-images", "helpdesk")}
alt={$text["services"]?.helpdesk?.imageAlt || "Helpdesk Pilt"}
:webpSrc="getOptimisedImagePath('home-page-images', 'helpdesk', 'webp')"
:src="getOptimisedImageFallback('home-page-images', 'helpdesk')"
:alt="text['services']?.helpdesk?.imageAlt || 'Helpdesk Pilt'"
objectFit="cover"
class="w-full h-full rounded-full object-[center_38%]"
pictureClass="block w-full h-full"
/>
</div>
<h3 class="w-full min-h-10 text-center text-2xl font-semibold flex items-center justify-center">{$text["services"]?.helpdesk?.title || "Helpdesk"}</h3>
<p class="w-full max-w-[26ch] text-center text-base">{$text["services"]?.helpdesk?.description || "HELPDESK on MTÜ Lapikute poolt pakutav arvutiabiteenus. Teenus on suunatud tudengitele, õppejõududele ning kõikidele huvilistele."}</p>
<h3 class="w-full min-h-10 text-center text-2xl font-semibold flex items-center justify-center">{{ text.services?.helpdesk?.title || "Helpdesk" }}</h3>
<p class="w-full max-w-[26ch] text-center text-base">{{ text.services?.helpdesk?.description || "HELPDESK on MTÜ Lapikute poolt pakutav arvutiabiteenus. Teenus on suunatud tudengitele, õppejõududele ning kõikidele huvilistele." }}</p>
</Center>
<Center dir="col" class="w-full max-w-sm justify-start mx-auto md:col-span-2 lg:col-span-1">
<div class="w-[min(72vw,240px)] md:w-[min(26vw,280px)] lg:w-[min(23vw,300px)] aspect-square rounded-full overflow-hidden">
<Image
webpSrc={getOptimisedImagePath("home-page-images", "company_temp", "webp")}
src={getOptimisedImageFallback("home-page-images", "company_temp")}
alt={$text["services"]?.company?.imageAlt || "Ettevõttele Pilt"}
:webpSrc="getOptimisedImagePath('home-page-images', 'company_temp', 'webp')"
:src="getOptimisedImageFallback('home-page-images', 'company_temp')"
:alt="text['services']?.company?.imageAlt || 'Ettevõttele Pilt'"
objectFit="cover"
class="w-full h-full rounded-full object-[center_25%]"
pictureClass="block w-full h-full"
/>
</div>
<h3 class="w-full min-h-10 text-center text-2xl font-semibold flex items-center justify-center">{$text["services"]?.company?.title || "Ettevõttele"}</h3>
<p class="w-full max-w-[26ch] text-center text-base">{$text["services"]?.company?.description || "Aitame sinu ideed ellu viia! Meie kogenud tudengid on valmis teie projekte realiseerima."}</p>
<h3 class="w-full min-h-10 text-center text-2xl font-semibold flex items-center justify-center">{{ text.services?.company?.title || "Ettevõttele" }}</h3>
<p class="w-full max-w-[26ch] text-center text-base">{{ text.services?.company?.description || "Aitame sinu ideed ellu viia! Meie kogenud tudengid on valmis teie projekte realiseerima." }}</p>
</Center>
</Grid>
</Section>
@@ -153,13 +150,13 @@
bg="bg-gray-900"
class="relative overflow-hidden text-white"
padding="none"
fullWidth={true}
:fullWidth="true"
contentClass="!px-0 !py-0"
>
<Grid
columns={2}
largeColumns={2}
mobileColumns={1}
:columns="2"
:largeColumns="2"
:mobileColumns="1"
gap="gap-y-0 md:gap-x-[clamp(5.75rem,9vw,9.75rem)] lg:gap-x-[clamp(6rem,9vw,10.5rem)]"
class=""
>
@@ -170,29 +167,29 @@
<div class="flex max-w-[54ch] flex-col justify-center gap-8">
<div class="space-y-5">
<h2 class="text-4xl font-semibold text-orange-500">
{$text["aboutSection"]?.title || "Meist"}
{{ text.aboutSection?.title || "Meist" }}
</h2>
<p class="text-base">
{$text["aboutSection"]?.description || "Viga teksti laadimisel. Palun proovi lehte uuesti laadida."}
{{ text.aboutSection?.description || "Viga teksti laadimisel. Palun proovi lehte uuesti laadida." }}
</p>
</div>
<div class="flex flex-col items-start gap-6">
<Button
class="rounded-md border-3 border-orange-500 bg-transparent px-10 py-2 transition-colors hover:bg-orange-500/10"
onClick={() => navigate($currentLang === "en" ? "/aboutus" : "/lapikutest")}
:onClick="() => navigate(currentLang === 'en' ? '/aboutus' : '/lapikutest')"
>
{$text["aboutSection"]?.moreInfo || "Rohkem infot"}
{{ text.aboutSection?.moreInfo || "Rohkem infot" }}
</Button>
<div class="flex items-center gap-4">
<Button
class="border-orange-500 bg-orange-500 px-20 py-2 transition-colors hover:border-orange-300 hover:bg-orange-300"
onClick={() => navigate($currentLang === "en" ? "/contact" : "/kontakt")}
:onClick="() => navigate(currentLang === 'en' ? '/contact' : '/kontakt')"
>
{$text["aboutSection"]?.contact || "Kontakt"}
{{ text.aboutSection?.contact || "Kontakt" }}
</Button>
<ArrowRight class="h-12 w-12 text-orange-500 md:h-14 md:w-14" strokeWidth={2.4} />
<ArrowRight class="h-12 w-12 text-orange-500 md:h-14 md:w-14" :strokeWidth="2.4" />
</div>
</div>
</div>
@@ -201,9 +198,9 @@
<div class="relative overflow-hidden min-h-90 md:h-full md:self-stretch">
<div class="absolute inset-0">
<Image
webpSrc={getOptimisedImagePath("home-page-images", "about_us", "webp")}
src={getOptimisedImageFallback("home-page-images", "about_us")}
alt={$text["aboutSection"]?.imageAlt || "Lapikud team"}
:webpSrc="getOptimisedImagePath('home-page-images', 'about_us', 'webp')"
:src="getOptimisedImageFallback('home-page-images', 'about_us')"
:alt="text['aboutSection']?.imageAlt || 'Lapikud team'"
objectFit="cover"
class="h-full w-full max-w-none!"
pictureClass="block h-full w-full"
@@ -232,7 +229,7 @@
<Section
class="relative overflow-hidden"
padding="none"
fullWidth={true}
:fullWidth="true"
contentClass="!px-0 !py-0"
>
<!-- Decorative SVGs -->
@@ -292,18 +289,18 @@
<!-- End of Decorative SVGs -->
<Grid
columns={2}
largeColumns={2}
mobileColumns={1}
:columns="2"
:largeColumns="2"
:mobileColumns="1"
gap="gap-y-0 md:gap-x-[clamp(5.75rem,9vw,9.75rem)] lg:gap-x-[clamp(6rem,9vw,10.5rem)]"
class="pb-10"
>
<div class="relative overflow-hidden min-h-90 md:min-h-0 md:h-full md:self-stretch">
<div class="absolute inset-0">
<Image
webpSrc={getOptimisedImagePath("home-page-images", "temp", "webp")}
src={getOptimisedImageFallback("home-page-images", "temp")}
alt={$text["whatWeDo"]?.imageAlt || "Lapikud parandamas riistvara"}
:webpSrc="getOptimisedImagePath('home-page-images', 'temp', 'webp')"
:src="getOptimisedImageFallback('home-page-images', 'temp')"
:alt="text['whatWeDo']?.imageAlt || 'Lapikud parandamas riistvara'"
objectFit="cover"
class="h-full w-full max-w-none!"
pictureClass="block h-full w-full"
@@ -318,28 +315,28 @@
<div class="ml-auto flex max-w-[54ch] flex-col justify-center gap-8 text-right">
<div class="space-y-5">
<h2 class="text-4xl font-semibold text-orange-500">
{$text["whatWeDo"]?.title || "Mida teeme"}
{{ text.whatWeDo?.title || "Mida teeme" }}
</h2>
<p class="text-base">
{$text["whatWeDo"]?.description || "Viga teksti laadimisel. Palun proovi lehte uuesti laadida."}
{{ text.whatWeDo?.description || "Viga teksti laadimisel. Palun proovi lehte uuesti laadida." }}
</p>
</div>
<div class="flex flex-col items-end gap-6">
<Button
class="rounded-md border-3 border-orange-500 bg-transparent px-10 py-2 transition-colors hover:bg-orange-500/10"
onClick={() => navigate($currentLang === "en" ? "/ourwork" : "/ettevottele")}
:onClick="() => navigate(currentLang === 'en' ? '/ourwork' : '/ettevottele')"
>
{$text["whatWeDo"]?.moreInfo || "Rohkem infot"}
{{ text.whatWeDo?.moreInfo || "Rohkem infot" }}
</Button>
<div class="flex items-center gap-4">
<ArrowLeft class="h-12 w-12 text-orange-500 md:h-14 md:w-14" strokeWidth={2.4} />
<ArrowLeft class="h-12 w-12 text-orange-500 md:h-14 md:w-14" :strokeWidth="2.4" />
<Button
class="border-orange-500 bg-orange-500 px-20 py-2 transition-colors hover:border-orange-300 hover:bg-orange-300"
onClick={() => navigate($currentLang === "en" ? "/student" : "/tudengile")}
:onClick="() => navigate(currentLang === 'en' ? '/student' : '/tudengile')"
>
{$text["whatWeDo"]?.contact || "Kontakt"}
{{ text.whatWeDo?.contact || "Kontakt" }}
</Button>
</div>
</div>
@@ -352,54 +349,55 @@
<Section>
<div class="text-center mb-12">
<h2 class="text-3xl text-orange-500">
{$text["partners"]?.title || "Koostööpartnerid"}
{{ text.partners?.title || "Koostööpartnerid" }}
</h2>
</div>
<Center class="flex-wrap">
{#each partners as partner}
{#if partner.url}
<template v-for="partner in partners">
<template v-if="partner.url">
<a
href={partner.url}
:href="partner.url"
target="_blank"
rel="noopener noreferrer"
class="flex h-[170px] items-center justify-center p-3 no-underline transition-all duration-300 hover:scale-105"
title={partner.name}
:title="partner.name"
>
{#if partner.image}
<template v-if="partner.image">
<img
src={getPartnerLogoPath(partner.image)}
alt={partner.name}
:src="getPartnerLogoPath(partner.image)"
:alt="partner.name"
class="w-auto max-h-[150px] transition-all duration-300 object-contain"
/>
{:else}
<span class="font-semibold text-center">{partner.name}</span>
{/if}
</template><template v-else>
<span class="font-semibold text-center">{{ partner.name }}</span>
</template>
</a>
{:else}
</template><template v-else>
<div class="flex h-[170px] items-center justify-center p-3 transition-all duration-300">
{#if partner.image}
<template v-if="partner.image">
<img
src={getPartnerLogoPath(partner.image)}
alt={partner.name}
:src="getPartnerLogoPath(partner.image)"
:alt="partner.name"
class="w-auto max-h-[150px] transition-all duration-300 object-contain"
/>
{:else}
<span class="font-semibold text-center">{partner.name}</span>
{/if}
</template><template v-else>
<span class="font-semibold text-center">{{ partner.name }}</span>
</template>
</div>
{/if}
{/each}
</template>
</template>
</Center>
<Center class="m-10">
<Button
class="rounded-md border-orange-500 bg-transparent px-20 py-3 transition-colors hover:bg-orange-500/10"
onClick={() => navigate("/partners")}
:onClick="() => navigate('/partners')"
>
{$text["partners"]?.moreInfo || "Rohkem infot partnerluste kohta"}
{{ text.partners?.moreInfo || "Rohkem infot partnerluste kohta" }}
</Button>
</Center>
</Section>
<!-- Spacer-->
<Section bg="bg-orange-500" class="h-3"/>
</template>

View File

@@ -1,22 +1,22 @@
<script>
<script setup>
import {
Section,
Grid,
Container,
Image,
} from "$components";
import { onMount, onDestroy } from "svelte";
} from "../components/index.js";
import { onMounted, ref } from 'vue';
import yaml from "js-yaml";
import { currentLang, getLangText, createPageTextStore } from "$lib";
import { getOptimisedImagePath, getOptimisedImageFallback } from "$lib/imageHelpers.js";
import Mail from "lucide-svelte/icons/mail";
import Phone from "lucide-svelte/icons/phone";
import { currentLang, getLangText, usePageText } from "../lib/index.js";
import { getOptimisedImagePath, getOptimisedImageFallback } from "../lib/imageHelpers.js";
import { Mail } from "@lucide/vue";
import { Phone } from "@lucide/vue";
let currentManagement = [];
let pastManagement = [];
let loading = true;
let loadError = false;
const text = createPageTextStore("Management");
const currentManagement = ref([]);
const pastManagement = ref([]);
const loading = ref(true);
const loadError = ref(false);
const text = usePageText("Management");
function parseYearLabel(label) {
const value = String(label ?? "");
@@ -43,32 +43,29 @@
);
}
onMount(async () => {
onMounted(async () => {
try {
const currentRes = await fetch('/_data/management.yml');
const currentYaml = await currentRes.text();
currentManagement = yaml.load(currentYaml) || [];
currentManagement.value = yaml.load(currentYaml) || [];
const pastRes = await fetch('/_data/past_management.yml');
const pastYaml = await pastRes.text();
pastManagement = yaml.load(pastYaml) || [];
pastManagement.value = yaml.load(pastYaml) || [];
} catch (error) {
console.error('Error loading management data:', error);
loadError = true;
loadError.value = true;
} finally {
loading = false;
loading.value = false;
}
});
onDestroy(() => {
text.destroy();
});
</script>
<template>
<div class="safe-area-navbar">
<Section
padding="none"
fullWidth={true}
:fullWidth="true"
contentClass="!px-0 !py-0"
class="overflow-hidden text-white"
>
@@ -78,9 +75,9 @@
<Container class="relative z-10 py-[clamp(3rem,8vw,6rem)]">
<div class="max-w-4xl">
<p class="mb-4 text-xs tracking-[0.16em] uppercase text-orange-500">{$text.hero?.eyebrow || "MTÜ Lapikud"}</p>
<h1 class="m-0 text-[clamp(2.4rem,6vw,4.2rem)] font-bold">{$text.hero?.title || "Management"}</h1>
<p class="mt-4 max-w-[52ch] text-[clamp(1.05rem,1.5vw,1.25rem)] text-white/75">{$text.hero?.intro || "People who keep the organisation moving forward."}</p>
<p class="mb-4 text-xs tracking-[0.16em] uppercase text-orange-500">{{ text.hero?.eyebrow || "MTÜ Lapikud" }}</p>
<h1 class="m-0 text-[clamp(2.4rem,6vw,4.2rem)] font-bold">{{ text.hero?.title || "Management" }}</h1>
<p class="mt-4 max-w-[52ch] text-[clamp(1.05rem,1.5vw,1.25rem)] text-white/75">{{ text.hero?.intro || "People who keep the organisation moving forward." }}</p>
</div>
</Container>
</div>
@@ -88,40 +85,40 @@
<!-- Current Management Section -->
<Section padding="large">
<h1 class="m-0 text-[clamp(1.8rem,3vw,2.6rem)] leading-[1.12] tracking-[-0.03em] mb-10">{$text.current?.label || "Current board"}</h1>
<h1 class="m-0 text-[clamp(1.8rem,3vw,2.6rem)] leading-[1.12] tracking-[-0.03em] mb-10">{{ text.current?.label || "Current board" }}</h1>
<Grid min="500px">
{#each currentManagement as member (member.name)}
<template v-for="member in currentManagement" :key="member.name">
<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">
<picture>
<source srcset={getOptimisedImagePath("management-images", member.photo, "webp")} type="image/webp" />
<source :srcset="getOptimisedImagePath('management-images', member.photo, 'webp')" type="image/webp" />
<img
src={getOptimisedImageFallback("management-images", member.photo)}
alt={member.name}
:src="getOptimisedImageFallback('management-images', member.photo)"
:alt="member.name"
class="w-full h-full object-cover rounded-md"
/>
</picture>
</div>
<div class="flex flex-col gap-2 grow">
<h3 class="text-2xl font-medium">{member.name}</h3>
<h3 class="text-2xl font-medium">{{ member.name }}</h3>
<p class="text-base text-gray-700">
{getLangText(member, 'role', $currentLang)} - {getLangText(member, 'subrole', $currentLang)}
{{ getLangText(member, 'role', currentLang) }} - {{ getLangText(member, 'subrole', currentLang) }}
</p>
<a
href="mailto:{member.email}"
:href="`mailto:${member.email}`"
class="text-base flex items-center gap-2 text-orange-500 hover:opacity-80 transition"
>
<Mail size={18} />{member.email}
<Mail :size="18" />{{ member.email }}
</a>
<a
href="tel:{member.phone}"
:href="`tel:${member.phone}`"
class="text-base flex items-center gap-2 text-orange-500 hover:opacity-80 transition"
>
<Phone size={18} />{member.phone}
<Phone :size="18" />{{ member.phone }}
</a>
</div>
</div>
{/each}
</template>
</Grid>
</Section>
@@ -129,52 +126,53 @@
<Section padding="large">
<div class="mb-7 flex flex-col items-start justify-between gap-6 md:flex-row md:items-end">
<div>
<h2 class="m-0 text-[clamp(1.8rem,3vw,2.6rem)] leading-[1.12] tracking-[-0.03em]">{$text.history?.label || "People who have led the organisation over time"}</h2>
<p class="mt-2 text-[0.98rem] text-[rgba(13,13,13,0.62)]">{$text.history?.hint || "Thanks to everyone who has helped guide Lapikud"}</p>
<h2 class="m-0 text-[clamp(1.8rem,3vw,2.6rem)] leading-[1.12] tracking-[-0.03em]">{{ text.history?.label || "People who have led the organisation over time" }}</h2>
<p class="mt-2 text-[0.98rem] text-[rgba(13,13,13,0.62)]">{{ text.history?.hint || "Thanks to everyone who has helped guide Lapikud" }}</p>
</div>
</div>
{#if loading}
<p class="m-0 py-4 text-black">{$text.loading || "Loading data..."}</p>
{:else if loadError}
<p class="m-0 py-4 text-red-500">{$text.error || "Failed to load the management data."}</p>
{:else}
<template v-if="loading">
<p class="m-0 py-4 text-black">{{ text.loading || "Loading data..." }}</p>
</template><template v-else-if="loadError">
<p class="m-0 py-4 text-red-500">{{ text.error || "Failed to load the management data." }}</p>
</template><template v-else>
<div class="flex flex-col gap-10">
{#each pastManagement as yearData (yearData.year)}
<template v-for="yearData in pastManagement" :key="yearData.year">
<section class="mb-5">
<div class="mb-4 flex w-full flex-col items-start gap-3 border-b border-orange-500 pb-3">
<div class="flex items-baseline gap-3 whitespace-nowrap">
<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}
<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}
<span class="text-[1.6rem] leading-none font-bold text-orange-500 tracking-[-0.04em]">{{ parseYearLabel(yearData.year).year }}</span>
<template v-if="parseYearLabel(yearData.year).tag">
<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>
</template>
</div>
</div>
<Grid min="150px" mobileColumns={2} gap="gap-5" class="items-start">
{#each yearData.members as member (member.name)}
<Grid min="150px" :mobileColumns="2" gap="gap-5" class="items-start">
<template v-for="member in yearData.members" :key="member.name">
<div class="group flex flex-col items-center gap-2.5 text-center">
<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}
<template v-if="member.image">
<Image
webpSrc={getOptimisedImagePath("past-management-images", member.image, "webp")}
src={getOptimisedImageFallback("past-management-images", member.image)}
alt={member.name}
:webpSrc="getOptimisedImagePath('past-management-images', member.image, 'webp')"
:src="getOptimisedImageFallback('past-management-images', member.image)"
:alt="member.name"
objectFit="cover"
class="block h-full w-full"
pictureClass="h-full w-full"
/>
{:else}
<div class="grid h-full w-full place-items-center text-sm font-bold text-orange-600">{getInitials(member.name)}</div>
{/if}
</template><template v-else>
<div class="grid h-full w-full place-items-center text-sm font-bold text-orange-600">{{ getInitials(member.name) }}</div>
</template>
</div>
<span class="max-w-[14ch] text-[0.86rem] font-semibold leading-[1.35] text-gray-900">{member.name}</span>
<span class="max-w-[14ch] text-[0.86rem] font-semibold leading-[1.35] text-gray-900">{{ member.name }}</span>
</div>
{/each}
</template>
</Grid>
</section>
{/each}
</template>
</div>
{/if}
</template>
</Section>
</div>
</template>

View File

@@ -1,27 +1,23 @@
<script>
import { Section, Container } from "$components";
import { onMount, onDestroy } from "svelte";
<script setup>
import { Section, Container } from "../components/index.js";
import { onMounted, ref } from 'vue';
import yaml from "js-yaml";
import { currentLang, getLangText, createPageTextStore } from "$lib";
import { getOptimisedImagePath, getOptimisedImageFallback } from "$lib/imageHelpers.js";
import { currentLang, getLangText, usePageText } from "../lib/index.js";
import { getOptimisedImagePath, getOptimisedImageFallback } from "../lib/imageHelpers.js";
import Mail from "lucide-svelte/icons/mail";
import Phone from "lucide-svelte/icons/phone";
import MessageCircle from "lucide-svelte/icons/message-circle";
import User from "lucide-svelte/icons/user";
import { Mail } from "@lucide/vue";
import { Phone } from "@lucide/vue";
import { MessageCircle } from "@lucide/vue";
import { User } from "@lucide/vue";
let mentors = [];
const text = createPageTextStore("Mentors");
const mentors = ref([]);
const text = usePageText("Mentors");
onMount(async () => {
onMounted(async () => {
const response = await fetch("/_data/mentors.yml");
const yamlText = await response.text();
mentors = yaml.load(yamlText);
});
onDestroy(() => {
text.destroy();
mentors.value = yaml.load(yamlText) || [];
});
function getContactIcon(contactType) {
@@ -53,11 +49,12 @@
}
</script>
<template>
<div class="safe-area-navbar">
<!-- Hero Section -->
<Section
padding="none"
fullWidth={true}
:fullWidth="true"
contentClass="!px-0 !py-0"
class="overflow-hidden text-white"
>
@@ -67,8 +64,8 @@
<Container class="relative z-10 py-[clamp(3rem,8vw,6rem)]">
<div class="max-w-4xl">
<h1 class="m-0 text-[clamp(2.4rem,6vw,4.2rem)] font-bold">{$text.hero?.title || "Our Mentors"}</h1>
<p class="mt-4 max-w-[52ch] text-[clamp(1.05rem,1.5vw,1.25rem)] text-white/75">{$text.hero?.intro || "Lapikud who are always ready to support you in your studies and personal development."}</p>
<h1 class="m-0 text-[clamp(2.4rem,6vw,4.2rem)] font-bold">{{ text.hero?.title || "Our Mentors" }}</h1>
<p class="mt-4 max-w-[52ch] text-[clamp(1.05rem,1.5vw,1.25rem)] text-white/75">{{ text.hero?.intro || "Lapikud who are always ready to support you in your studies and personal development." }}</p>
</div>
</Container>
</div>
@@ -77,119 +74,115 @@
<!-- Mentors Grid -->
<Section>
<div class="flex flex-col gap-8">
{#each mentors as mentor}
<template v-for="mentor in mentors">
<div>
<div class="flex flex-col md:flex-row gap-6 m-3">
<!-- Mentor Photo -->
{#if mentor.photo}
<template v-if="mentor.photo">
<div class="shrink-0">
<picture>
<source srcset={getOptimisedImagePath("mentors-images", mentor.photo, "webp")} type="image/webp" />
<source :srcset="getOptimisedImagePath('mentors-images', mentor.photo, 'webp')" type="image/webp" />
<img
src={getOptimisedImageFallback("mentors-images", mentor.photo)}
alt={mentor.name}
:src="getOptimisedImageFallback('mentors-images', mentor.photo)"
:alt="mentor.name"
class="w-full md:w-48 h-48 object-cover"
/>
</picture>
</div>
{/if}
</template>
<!-- Mentor Info -->
<div class="flex-1">
<h3 class="text-2xl font-bold mb-2">{mentor.name}</h3>
<h3 class="text-2xl font-bold mb-2">{{ mentor.name }}</h3>
{#if mentor.age}
<template v-if="mentor.age">
<p class="text-gray-600 mb-2">
Vanus: {mentor.age}
Vanus: {{ mentor.age }}
</p>
{/if}
</template>
{#if mentor.speciality}
<template v-if="mentor.speciality">
<p
class="text-lg font-semibold mb-2"
style="color: var(--orange)"
>
{getLangText(
{{ getLangText(
mentor,
"speciality",
$currentLang,
)}
currentLang,
) }}
</p>
{/if}
</template>
{#if mentor.teams && mentor.teams.length > 0}
<template v-if="mentor.teams && mentor.teams.length > 0">
<div class="mb-3">
<span class="font-semibold">Meeskonnad: </span>
<span class="text-gray-700"
>{mentor.teams.join(", ")}</span
>{{ mentor.teams.join(", ") }}</span
>
</div>
{/if}
</template>
{#if mentor.term}
<template v-if="mentor.term">
<p class="text-gray-600 mb-3">
Ametiperiood: {mentor.term}
{mentor.term === 1 ? "aasta" : "aastat"}
Ametiperiood: {{ mentor.term }}
{{ mentor.term === 1 ? "aasta" : "aastat" }}
</p>
{/if}
</template>
{#if mentor.description}
<template v-if="mentor.description">
<p
class="text-sm font-semibold mb-2"
style="color: var(--orange)"
>
Otsin: {getLangText(
Otsin: {{ getLangText(
mentor,
"description",
$currentLang,
)}
currentLang,
) }}
</p>
{/if}
</template>
{#if mentor.activities}
<template v-if="mentor.activities">
<p class="text-gray-700 text-sm mb-4">
{getLangText(
{{ getLangText(
mentor,
"activities",
$currentLang,
)}
currentLang,
) }}
</p>
{/if}
</template>
<!-- Contact Methods -->
{#if mentor.contactMethods && mentor.contactMethods.length > 0}
<template v-if="mentor.contactMethods && mentor.contactMethods.length > 0">
<div class="flex flex-wrap gap-2 mt-4">
{#each mentor.contactMethods as method}
{@const IconComponent = getContactIcon(
method.name,
)}
<template v-for="method in mentor.contactMethods">
<a
href={getContactLink(method)}
:href="getContactLink(method)"
class="inline-flex items-center gap-2 px-3 py-2 bg-orange-100 hover:bg-orange-200 rounded-lg transition-colors text-sm"
title={method.name}
target={method.value.startsWith("http")
? "_blank"
: "_self"}
rel={method.value.startsWith("http")
? "noopener noreferrer"
: ""}
:title="method.name"
:target="method.value.startsWith('http')
? '_blank'
: '_self'"
:rel="method.value.startsWith('http')
? 'noopener noreferrer'
: ''"
>
<svelte:component
this={IconComponent}
size={16}
<component :is="getContactIcon(method.name)"
:size="16"
/>
<span class="capitalize"
>{method.name}</span
>{{ method.name }}</span
>
</a>
{/each}
</template>
</div>
{/if}
</template>
</div>
</div>
</div>
{/each}
</template>
</div>
</Section>
</div>
</template>

View File

@@ -1,35 +1,31 @@
<script>
import { Section, Grid, Card, Button } from "$components";
import { onMount, onDestroy } from "svelte";
<script setup>
import { Section, Grid, Card, Button } from "../components/index.js";
import { computed, onMounted, ref } from 'vue';
import yaml from "js-yaml";
import { currentLang, getLangText, createPageTextStore } from "$lib";
import { getOptimisedImagePath, getOptimisedImageFallback } from "$lib/imageHelpers.js";
import { currentLang, getLangText, usePageText } from "../lib/index.js";
import { getOptimisedImagePath, getOptimisedImageFallback } from "../lib/imageHelpers.js";
import ExternalLink from "lucide-svelte/icons/external-link";
import { ExternalLink } from "@lucide/vue";
let projects = [];
const text = createPageTextStore("OurWork");
const projects = ref([]);
const text = usePageText("OurWork");
onMount(async () => {
onMounted(async () => {
const response = await fetch("/_data/ourwork.yml");
const yamlText = await response.text();
const parsed = yaml.load(yamlText) || [];
projects = parsed.filter((p) => p?.title);
projects.value = parsed.filter((p) => p?.title);
});
onDestroy(() => {
text.destroy();
});
$: featuredProject = projects[0] || null;
$: regularProjects = projects.slice(1);
const featuredProject = computed(() => projects.value[0] || null);
const regularProjects = computed(() => projects.value.slice(1));
function projectTitle(project) {
return getLangText(project, "title", $currentLang);
return getLangText(project, "title", currentLang.value);
}
function projectDescription(project) {
return getLangText(project, "description", $currentLang);
return getLangText(project, "description", currentLang.value);
}
function projectImage(project) {
@@ -51,6 +47,7 @@
}
</script>
<template>
<div class="safe-area-navbar">
<!-- Hero Section -->
@@ -62,12 +59,12 @@
>
<div class="relative z-10">
<h1 class="max-w-[20ch] text-[clamp(2.6rem,5.5vw,4.8rem)] leading-none font-bold tracking-[-0.03em] text-white">
{$text["intro"]?.prefix || "Meie tehtud "}
{{ text.intro?.prefix || "Meie tehtud " }}
<br />
<span class="text-orange-500">{$text["intro"]?.emphasis || "tööd."}</span>
<span class="text-orange-500">{{ text.intro?.emphasis || "tööd." }}</span>
</h1>
<p class="mt-5 max-w-105 text-[0.95rem] leading-[1.7] text-white/45">
{$text["intro"]?.subtitle || "Valik projekte, mida meie liikmed on teostanud, veebilehtedest mobiilirakendusteni."}
{{ text.intro?.subtitle || "Valik projekte, mida meie liikmed on teostanud, veebilehtedest mobiilirakendusteni." }}
</p>
</div>
</Section>
@@ -81,25 +78,25 @@
<div class="flex flex-col items-start justify-between gap-6 lg:flex-row lg:items-center lg:gap-10">
<div>
<h2 class="mb-2 text-[clamp(1.4rem,2.5vw,2rem)] leading-[1.1] font-bold tracking-[-0.02em] text-gray-900">
{$text["cta"]?.title || "Alustame koostööd?"}
{{ text.cta?.title || "Alustame koostööd?" }}
</h2>
<p class="max-w-95 text-[0.88rem] leading-[1.6] text-gray-900/70">
{$text["cta"]?.description || "Võta ühendust ja arutame, kuidas saame sinu ideed ellu viia!"}
{{ text.cta?.description || "Võta ühendust ja arutame, kuidas saame sinu ideed ellu viia!" }}
</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<Button
onClick={openCtaEmail}
:onClick="openCtaEmail"
class="gap-2 rounded-none! border-gray-900! bg-gray-900 px-5.5 py-3 text-[0.83rem] font-semibold text-white transition-colors hover:border-gray-800 hover:bg-gray-800"
>
{$text["cta"]?.emailButton || "Saada e-kiri"} <span aria-hidden="true">&rarr;</span>
{{ text.cta?.emailButton || "Saada e-kiri" }} <span aria-hidden="true">&rarr;</span>
</Button>
<Button
onClick={openCtaContact}
:onClick="openCtaContact"
class="rounded-none! border-gray-900/35! bg-transparent px-5.5 py-3 text-[0.83rem] font-semibold text-gray-900 transition-colors hover:border-gray-900 hover:bg-gray-900/6"
>
{$text["cta"]?.contactButton || "Kontaktandmed"}
{{ text.cta?.contactButton || "Kontaktandmed" }}
</Button>
</div>
</div>
@@ -107,92 +104,93 @@
<Section padding="large">
<div class="mb-22">
<p class="mb-3 text-[0.72rem] font-bold uppercase tracking-[0.12em] text-orange-500">{$text["portfolio"]?.label || "Tehtud tööd"}</p>
<h1 class="m-0 max-w-[20ch] text-[clamp(1.9rem,4vw,3.15rem)] leading-[1.08] tracking-[-0.03em]">{$text["portfolio"]?.title || "Projektid, mis räägivad enda eest."}</h1>
<p class="mb-3 text-[0.72rem] font-bold uppercase tracking-[0.12em] text-orange-500">{{ text.portfolio?.label || "Tehtud tööd" }}</p>
<h1 class="m-0 max-w-[20ch] text-[clamp(1.9rem,4vw,3.15rem)] leading-[1.08] tracking-[-0.03em]">{{ text.portfolio?.title || "Projektid, mis räägivad enda eest." }}</h1>
<p class="mt-4 max-w-[62ch] leading-[1.65] text-gray-900">
{$text["portfolio"]?.subtitle || "Igal projektil on oma lugu. Siin on valik töödest, mida meie liikmed on aastate jooksul ellu viinud."}
{{ text.portfolio?.subtitle || "Igal projektil on oma lugu. Siin on valik töödest, mida meie liikmed on aastate jooksul ellu viinud." }}
</p>
</div>
{#if featuredProject}
<template v-if="featuredProject">
<Card class="mb-5! grid! gap-4 rounded-none! border-0! bg-white! p-0! md:grid-cols-[1.25fr_1fr]! md:gap-6" variant="default">
<div class="flex min-h-[220px] items-center justify-center md:min-h-[280px]">
{#if featuredProject.photo}
<template v-if="featuredProject.photo">
<img
src={projectImage(featuredProject)}
alt={projectTitle(featuredProject)}
:src="projectImage(featuredProject)"
:alt="projectTitle(featuredProject)"
class="block h-auto max-h-full w-auto max-w-full object-contain object-center shadow-[0_1px_2px_rgba(13,13,13,0.05)]"
on:error={(event) => handleImageError(event, featuredProject.photo)}
@error="(event) => handleImageError(event, featuredProject.photo)"
/>
{:else}
<div class="grid h-full min-h-[180px] w-full place-items-center text-[0.85rem] text-gray-900">{$text["portfolio"]?.placeholder || "Pilt puudub"}</div>
{/if}
</template><template v-else>
<div class="grid h-full min-h-[180px] w-full place-items-center text-[0.85rem] text-gray-900">{{ text.portfolio?.placeholder || "Pilt puudub" }}</div>
</template>
</div>
<div class="flex h-full flex-1 flex-col justify-between gap-[clamp(0.75rem,2vw,1.5rem)] p-[clamp(1.5rem,3vw,2.5rem)]">
<div class="flex flex-col gap-[clamp(0.45rem,1vw,0.8rem)]">
<h2 class="m-0 text-[clamp(1.45rem,2.7vw,2rem)] leading-[1.2] tracking-[-0.02em]">{projectTitle(featuredProject)}</h2>
{#if projectDescription(featuredProject)}
<p class="m-0 text-[0.9rem] leading-[1.6] text-gray-900">{projectDescription(featuredProject)}</p>
{/if}
<h2 class="m-0 text-[clamp(1.45rem,2.7vw,2rem)] leading-[1.2] tracking-[-0.02em]">{{ projectTitle(featuredProject) }}</h2>
<template v-if="projectDescription(featuredProject)">
<p class="m-0 text-[0.9rem] leading-[1.6] text-gray-900">{{ projectDescription(featuredProject) }}</p>
</template>
</div>
{#if featuredProject.url}
<template v-if="featuredProject.url">
<a
href={featuredProject.url}
:href="featuredProject.url"
target="_blank"
rel="noopener noreferrer"
class="mt-auto inline-flex w-fit items-center gap-1.5 pt-1 font-semibold text-orange-500 hover:underline"
>
{$text["portfolio"]?.projectLink || "Vaata projekti"}
<ExternalLink size={16} />
{{ text.portfolio?.projectLink || "Vaata projekti" }}
<ExternalLink :size="16" />
</a>
{/if}
</template>
</div>
</Card>
{/if}
</template>
<div class="ourwork-grid">
{#each regularProjects as project}
<template v-for="project in regularProjects">
<Card variant="default" class="ourwork-grid-card gap-4 rounded-none! border-0! bg-white! p-0!">
<div class="flex aspect-16/10 items-center justify-center">
{#if project.photo}
<template v-if="project.photo">
<img
src={projectImage(project)}
alt={projectTitle(project)}
:src="projectImage(project)"
:alt="projectTitle(project)"
class="block h-auto max-h-full w-auto max-w-full object-contain object-center shadow-[0_1px_2px_rgba(13,13,13,0.05)]"
on:error={(event) => handleImageError(event, project.photo)}
@error="(event) => handleImageError(event, project.photo)"
/>
{:else}
<div class="grid h-full min-h-[180px] w-full place-items-center text-[0.85rem] text-gray-900">{$text["portfolio"]?.placeholder || "Pilt puudub"}</div>
{/if}
</template><template v-else>
<div class="grid h-full min-h-[180px] w-full place-items-center text-[0.85rem] text-gray-900">{{ text.portfolio?.placeholder || "Pilt puudub" }}</div>
</template>
</div>
<div class="ourwork-card-content flex h-full flex-1 flex-col justify-between gap-[clamp(0.55rem,1vw,0.95rem)] px-5 pb-5 pt-1.5">
<div class="flex flex-col gap-[clamp(0.4rem,0.9vw,0.75rem)]">
<h3 class="ourwork-card-title m-0 text-base leading-[1.2] tracking-[-0.02em]">{projectTitle(project)}</h3>
{#if projectDescription(project)}
<p class="ourwork-card-description m-0 text-[0.9rem] leading-[1.6] text-gray-900">{projectDescription(project)}</p>
{:else}
<p class="ourwork-card-description m-0 text-[0.9rem] leading-[1.6] text-gray-900 italic">{$text["portfolio"]?.noDescription || "Kirjeldus lisamisel."}</p>
{/if}
<h3 class="ourwork-card-title m-0 text-base leading-[1.2] tracking-[-0.02em]">{{ projectTitle(project) }}</h3>
<template v-if="projectDescription(project)">
<p class="ourwork-card-description m-0 text-[0.9rem] leading-[1.6] text-gray-900">{{ projectDescription(project) }}</p>
</template><template v-else>
<p class="ourwork-card-description m-0 text-[0.9rem] leading-[1.6] text-gray-900 italic">{{ text.portfolio?.noDescription || "Kirjeldus lisamisel." }}</p>
</template>
</div>
{#if project.url}
<template v-if="project.url">
<a
href={project.url}
:href="project.url"
target="_blank"
rel="noopener noreferrer"
class="mt-auto inline-flex w-fit items-center gap-1.5 pt-1 font-semibold text-orange-500 hover:underline"
>
{$text["portfolio"]?.projectLink || "Vaata projekti"}
<ExternalLink size={16} />
{{ text.portfolio?.projectLink || "Vaata projekti" }}
<ExternalLink :size="16" />
</a>
{/if}
</template>
</div>
</Card>
{/each}
</template>
</div>
</Section>
</div>
</template>
<style>
.ourwork-grid {
@@ -202,7 +200,7 @@
grid-template-columns: 1fr;
}
.ourwork-grid :global(.ourwork-grid-card) {
.ourwork-grid .ourwork-grid-card {
display: flex;
flex-direction: column;
height: 100%;

View File

@@ -1,39 +0,0 @@
<script>
import { Section, Grid } from "$components";
import { onDestroy } from "svelte";
import { createPageTextStore } from "$lib";
let sectionElement;
let imagesLoaded = 0;
const totalImages = 3;
const text = createPageTextStore("Striim");
function handleImageLoad() {
imagesLoaded++;
if (imagesLoaded === totalImages && sectionElement) {
sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
onDestroy(() => {
text.destroy();
});
</script>
<div class="safe-area-navbar" bind:this={sectionElement}>
<Section fullWidth={true} contentClass="flex items-start">
<Grid min="400px" class="w-full">
<a href="https://master.lapikud.ee/stream/1/stream">
<img src="https://master.lapikud.ee/stream/1/substream" alt={$text.streamAlts?.first || "camera 1 stream"} style="width:100%;" on:load={handleImageLoad}/>
</a>
<a href="https://master.lapikud.ee/stream/2/stream">
<img src="https://master.lapikud.ee/stream/2/substream" alt={$text.streamAlts?.second || "camera 2 stream"} style="width:100%;" on:load={handleImageLoad}/>
</a>
<a href="https://master.lapikud.ee/stream/3/stream">
<img src="https://master.lapikud.ee/stream/3/substream" alt={$text.streamAlts?.third || "camera 3 stream"} style="width:100%;" on:load={handleImageLoad}/>
</a>
</Grid>
</Section>
</div>

38
src/routes/Striim.vue Normal file
View File

@@ -0,0 +1,38 @@
<script setup>
import { Section, Grid } from "../components/index.js";
import { ref } from 'vue';
import { usePageText } from "../lib/index.js";
const sectionElement = ref(null);
const imagesLoaded = ref(0);
const totalImages = 3;
const text = usePageText("Striim");
function handleImageLoad() {
imagesLoaded.value++;
if (imagesLoaded.value === totalImages && sectionElement.value) {
sectionElement.value.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
</script>
<template>
<div class="safe-area-navbar" ref="sectionElement">
<Section :fullWidth="true" contentClass="flex items-start">
<Grid min="400px" class="w-full">
<a href="https://master.lapikud.ee/stream/1/stream">
<img src="https://master.lapikud.ee/stream/1/substream" :alt="text.streamAlts?.first || 'camera 1 stream'" style="width:100%;" @load="handleImageLoad"/>
</a>
<a href="https://master.lapikud.ee/stream/2/stream">
<img src="https://master.lapikud.ee/stream/2/substream" :alt="text.streamAlts?.second || 'camera 2 stream'" style="width:100%;" @load="handleImageLoad"/>
</a>
<a href="https://master.lapikud.ee/stream/3/stream">
<img src="https://master.lapikud.ee/stream/3/substream" :alt="text.streamAlts?.third || 'camera 3 stream'" style="width:100%;" @load="handleImageLoad"/>
</a>
</Grid>
</Section>
</div>
</template>

View File

@@ -1,31 +1,27 @@
<script>
<script setup>
import {
Section,
Grid,
Button,
} from "$components";
import { onMount, onDestroy } from "svelte";
} from "../components/index.js";
import { computed, onMounted, ref } from 'vue';
import yaml from 'js-yaml';
import { createPageTextStore } from "$lib";
import { usePageText } from "../lib/index.js";
let members = { junior: [], senior: [] };
const text = createPageTextStore("Student");
const members = ref({ junior: [], senior: [] });
const text = usePageText("Student");
const joinFormUrl = "https://pilves.lapikud.ee/apps/forms/s/WXed8sbG2s45GMKGAiXCemgE";
onMount(async () => {
onMounted(async () => {
try {
const response = await fetch('/_data/members.yml');
const yamlText = await response.text();
members = yaml.load(yamlText) || { junior: [], senior: [] };
members.value = yaml.load(yamlText) || { junior: [], senior: [] };
} catch {
members = { junior: [], senior: [] };
members.value = { junior: [], senior: [] };
}
});
onDestroy(() => {
text.destroy();
});
const scrollToJoin = () => {
document.getElementById("liitu")?.scrollIntoView({ behavior: "smooth", block: "start" });
};
@@ -38,41 +34,42 @@
window.open(joinFormUrl, "_blank", "noopener,noreferrer");
};
$: teamSections = ($text["teams"]?.items || []).map((team, index) => ({
const teamSections = computed(() => (text.value.teams?.items || []).map((team, index) => ({
...team,
number: team.number || `0${index + 1}`,
}));
})));
$: memberSections = [
{ key: "junior", label: $text["members"]?.junior || "" },
{ key: "senior", label: $text["members"]?.senior || "" },
{ key: "graduates", label: $text["members"]?.graduates || "" },
];
const memberSections = computed(() => [
{ key: "junior", label: text.value.members?.junior || '' },
{ key: "senior", label: text.value.members?.senior || '' },
{ key: "graduates", label: text.value.members?.graduates || '' },
]);
</script>
<template>
<div class="safe-area-navbar">
<Section class="overflow-hidden bg-gray-900" padding="none" fullWidth={true} contentClass="relative !px-0">
<Section class="overflow-hidden bg-gray-900" padding="none" :fullWidth="true" contentClass="relative !px-0">
<div class="relative overflow-hidden py-[3rem]">
<div class="relative z-10 mx-auto w-full max-w-(--page-max-width) px-(--page-padding-inline)">
<div class="relative z-10 w-full max-w-3xl">
<h1 class="mb-6 text-[clamp(2.5rem,7vw,4.75rem)] leading-[1.03] font-medium text-white">
{$text["hero"]?.title || ""}<br />
<span class="italic text-orange-500">{$text["hero"]?.emphasis || ""}</span>
{{ text.hero?.title || '' }}<br />
<span class="italic text-orange-500">{{ text.hero?.emphasis || '' }}</span>
</h1>
<p class="mb-10 max-w-2xl text-base leading-8 text-white/55">
{$text["hero"]?.description || ""}
{{ text.hero?.description || '' }}
</p>
<div class="flex flex-wrap items-center gap-4">
<Button onClick={scrollToJoin} class="border-transparent bg-orange-500 px-8 py-3 text-sm font-semibold text-white hover:opacity-90">
{$text["hero"]?.cta || ""}
<Button :onClick="scrollToJoin" class="border-transparent bg-orange-500 px-8 py-3 text-sm font-semibold text-white hover:opacity-90">
{{ text.hero?.cta || '' }}
</Button>
<a href="#tiimid" on:click|preventDefault={scrollToUnderHero} class="text-sm text-white/40">{$text["hero"]?.aside || ""}</a>
<a href="#tiimid" @click.prevent="scrollToUnderHero" class="text-sm text-white/40">{{ text.hero?.aside || '' }}</a>
</div>
</div>
</div>
<div class="pointer-events-none select-none absolute right-0 bottom-0 translate-y-1/4 text-[clamp(8rem,20vw,14rem)] leading-none italic text-white/4">
{$text["hero"]?.bgWord || "Lapikud"}
{{ text.hero?.bgWord || "Lapikud" }}
</div>
</div>
</Section>
@@ -81,77 +78,77 @@
<Section id="tiimid" class="bg-white" padding="large">
<div class="mb-14">
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.13em] text-orange-500">{$text["teams"]?.kicker || ""}</p>
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.13em] text-orange-500">{{ text.teams?.kicker || '' }}</p>
<h2 class="mb-4 max-w-4xl text-[clamp(2rem,4vw,3rem)] leading-tight font-medium text-black">
{$text["teams"]?.title || ""}
{{ text.teams?.title || '' }}
</h2>
<p class="max-w-2xl text-base leading-8 text-black/60">
{$text["teams"]?.subtitle || ""}
{{ text.teams?.subtitle || '' }}
</p>
</div>
<div class="flex flex-col">
{#each teamSections as team, index}
<template v-for="(team, index) in teamSections">
<div class="grid border-t border-black/10 py-12 last:border-b xl:grid-cols-[240px_1fr_1fr]">
<div class="pr-10">
<h3 class="my-3 text-4xl leading-none font-medium text-black">{team.title}</h3>
<p class="text-sm leading-5 text-black/55">{team.short || ""}</p>
<h3 class="my-3 text-4xl leading-none font-medium text-black">{{ team.title }}</h3>
<p class="text-sm leading-5 text-black/55">{{ team.short || '' }}</p>
</div>
<div class="pt-8 xl:border-l xl:border-black/10 xl:px-12 xl:pt-0">
{#each team.about || [] as paragraph}
<p class="mb-5 text-[0.95rem] leading-8 text-black/80 last:mb-0">{paragraph}</p>
{/each}
<template v-for="paragraph in team.about || []">
<p class="mb-5 text-[0.95rem] leading-8 text-black/80 last:mb-0">{{ paragraph }}</p>
</template>
<div class="mt-6 flex flex-wrap gap-2">
{#each team.tags || [] as tag}
<span class="px-3 py-1.5 text-xs font-medium text-orange-500">{tag}</span>
{/each}
<template v-for="tag in team.tags || []">
<span class="px-3 py-1.5 text-xs font-medium text-orange-500">{{ tag }}</span>
</template>
</div>
</div>
<div class="pt-8 xl:border-l xl:border-black/10 xl:pl-12 xl:pt-0">
<h4 class="mb-4 text-xs font-semibold uppercase tracking-widest text-black">{team.whatTitle || ""}</h4>
<h4 class="mb-4 text-xs font-semibold uppercase tracking-widest text-black">{{ team.whatTitle || '' }}</h4>
<ul class="mb-8 space-y-2">
{#each team.what || [] as item}
<template v-for="item in team.what || []">
<li class="relative pl-4 text-sm leading-6 text-black/80 before:absolute before:left-0 before:top-0.5 before:text-[0.65rem] before:text-orange-500 before:content-['—']">
{item}
{{ item }}
</li>
{/each}
</template>
</ul>
<h4 class="mb-4 text-xs font-semibold uppercase tracking-widest text-black">{team.eventsTitle || ""}</h4>
<h4 class="mb-4 text-xs font-semibold uppercase tracking-widest text-black">{{ team.eventsTitle || '' }}</h4>
<ul class="space-y-2">
{#each team.events || [] as event}
<template v-for="event in team.events || []">
<li class="relative pl-4 text-sm leading-6 text-black/80 before:absolute before:left-0 before:top-0.5 before:text-[0.65rem] before:text-black/50 before:content-['↻']">
{event}
{{ event }}
</li>
{/each}
</template>
</ul>
</div>
</div>
{/each}
</template>
</div>
</Section>
<Section id="liitu" class="overflow-hidden bg-orange-500" padding="large">
<div class="relative grid max-w-5xl items-center gap-10 lg:grid-cols-[1fr_auto]">
<div class="pointer-events-none select-none absolute -right-6 -top-10 text-[clamp(7rem,16vw,12rem)] leading-none italic text-black/10">
{$text["join"]?.bgWord || ""}
{{ text.join?.bgWord || '' }}
</div>
<div class="relative">
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.13em] text-black/45">{$text["join"]?.kicker || ""}</p>
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.13em] text-black/45">{{ text.join?.kicker || '' }}</p>
<h2 class="mb-4 text-[clamp(2rem,4vw,3.3rem)] leading-tight font-medium text-black">
{$text["join"]?.title || ""}<br />{$text["join"]?.titleLine2 || ""}
{{ text.join?.title || '' }}<br />{{ text.join?.titleLine2 || '' }}
</h2>
<p class="max-w-2xl text-base leading-8 text-black/70">
{$text["join"]?.description || ""}
{{ text.join?.description || '' }}
</p>
</div>
<div class="relative">
<Button onClick={openJoinForm} class="border-transparent bg-black px-9 py-3 text-base font-semibold text-white hover:opacity-90">
{$text["join"]?.cta || ""}
<Button :onClick="openJoinForm" class="border-transparent bg-black px-9 py-3 text-base font-semibold text-white hover:opacity-90">
{{ text.join?.cta || '' }}
</Button>
</div>
</div>
@@ -161,25 +158,26 @@
<Section id="liikmed" class="bg-[#f7f6f3]" padding="large">
<div class="mb-10">
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.13em] text-orange-500">{$text["members"]?.kicker || ""}</p>
<p class="mb-3 text-xs font-semibold uppercase tracking-[0.13em] text-orange-500">{{ text.members?.kicker || '' }}</p>
<h2 class="text-[clamp(2rem,3.6vw,2.9rem)] leading-tight font-medium text-black">
{$text["members"]?.title || ""}
{{ text.members?.title || '' }}
</h2>
</div>
{#each memberSections as section, idx}
{#if members[section.key] && members[section.key].length > 0}
<div class={idx < memberSections.length - 1 ? "mb-12" : ""}>
<template v-for="(section, idx) in memberSections">
<template v-if="members[section.key] && members[section.key].length > 0">
<div :class="idx < memberSections.length - 1 ? 'mb-12' : ''">
<h3 class="mb-5 border-b border-orange-500/30 pb-3 text-xs font-semibold uppercase tracking-widest text-orange-500">
{section.label}
{{ section.label }}
</h3>
<Grid columns={5} tabletColumns={4} mobileColumns={2} gap="gap-x-4 gap-y-2">
{#each members[section.key] as member}
<span class="py-1 text-sm leading-6 text-black/80">{member}</span>
{/each}
<Grid :columns="5" :tabletColumns="4" :mobileColumns="2" gap="gap-x-4 gap-y-2">
<template v-for="member in members[section.key]">
<span class="py-1 text-sm leading-6 text-black/80">{{ member }}</span>
</template>
</Grid>
</div>
{/if}
{/each}
</template>
</template>
</Section>
</div>
</template>

View File

@@ -1,26 +1,22 @@
<script>
import { Section, Stack, Image } from "$components";
import { onMount, onDestroy } from "svelte";
<script setup>
import { Section, Stack, Image } from "../components/index.js";
import { onMounted, ref } from 'vue';
import yaml from "js-yaml";
import { currentLang, getLangText, createPageTextStore } from "$lib";
import { getRootAssetPath } from "$lib/imageHelpers.js";
import { currentLang, getLangText, usePageText } from "../lib/index.js";
import { getRootAssetPath } from "../lib/imageHelpers.js";
let workshops = [];
const text = createPageTextStore("Workshops");
const workshops = ref([]);
const text = usePageText("Workshops");
onMount(async () => {
onMounted(async () => {
const response = await fetch("/_data/workshops.yml");
const yamlText = await response.text();
const parsed = yaml.load(yamlText) || [];
workshops = parsed.filter((w) => w?.title);
});
onDestroy(() => {
text.destroy();
workshops.value = parsed.filter((w) => w?.title);
});
function getField(workshop, field) {
return getLangText(workshop, field, $currentLang);
return getLangText(workshop, field, currentLang.value);
}
function getGallery(workshop) {
@@ -48,21 +44,22 @@
}
</script>
<template>
<div class="safe-area-navbar">
<!-- Hero Section -->
<Section bg="bg-gray-900" class="text-white">
<div class="max-w-2xl">
<p class="font-dm-mono text-xs uppercase tracking-widest text-orange-500 mb-4">
{$text["hero"]?.label}
{{ text.hero?.label }}
</p>
<h1 class="font-syne text-5xl md:text-6xl lg:text-7xl font-bold leading-tight mb-4">
{$text["hero"]?.title}
{{ text.hero?.title }}
<em class="block text-orange-500 not-italic"
>{@html $text["hero"]?.titleHighlight}</em
><span v-html="text.hero?.titleHighlight"></span></em
>
</h1>
<p class="text-sm text-gray-400 max-w-sm leading-relaxed font-space-grotesk font-light">
{$text["hero"]?.subtitle}
{{ text.hero?.subtitle }}
</p>
</div>
</Section>
@@ -70,66 +67,67 @@
<!-- Workshops Content -->
<Section>
<Stack gap="lg">
{#each workshops as workshop, index (index)}
<div class={`grid grid-cols-1 md:grid-cols-2 gap-14 md:gap-16 py-16 border-b border-orange-500 ${index === workshops.length - 1 ? "border-b-0" : ""}`}>
<template v-for="(workshop, index) in workshops" :key="index">
<div :class="`grid grid-cols-1 md:grid-cols-2 gap-14 md:gap-16 py-16 border-b border-orange-500 ${index === workshops.length - 1 ? 'border-b-0' : ''}`">
<!-- Gallery -->
<div
class={`grid gap-1.5 ${index % 2 === 1 ? "md:order-2" : ""}`}
:class="`grid gap-1.5 ${index % 2 === 1 ? 'md:order-2' : ''}`"
>
<div class={`grid ${getGalleryClass(getGallery(workshop).length)} gap-1.5`}>
{#each getGallery(workshop) as filename, i (filename)}
<div :class="`grid ${getGalleryClass(getGallery(workshop).length)} gap-1.5`">
<template v-for="(filename, i) in getGallery(workshop)" :key="filename">
<div
class={`overflow-hidden ${getImageHeightClass(getGallery(workshop).length, i)}`}
:class="`overflow-hidden ${getImageHeightClass(getGallery(workshop).length, i)}`"
>
<Image
src={getRootAssetPath("workshop-images", filename, "jpg")}
alt={`${getField(workshop, "title")} ${i + 1}`}
:src="getRootAssetPath('workshop-images', filename, 'jpg')"
:alt="`${getField(workshop, 'title')} ${i + 1}`"
objectFit="cover"
class="h-full w-full"
pictureClass="h-full w-full"
/>
</div>
{/each}
</template>
</div>
</div>
<!-- Content -->
<div class={`pt-2 ${index % 2 === 1 ? "md:order-1" : ""}`}>
<div :class="`pt-2 ${index % 2 === 1 ? 'md:order-1' : ''}`">
<h2 class="font-syne text-2xl md:text-3xl font-bold text-gray-900 mb-4">
{getField(workshop, "title")}
{{ getField(workshop, "title") }}
</h2>
<p class="text-sm leading-7 text-gray-600 font-space-grotesk font-light mb-7">
{getField(workshop, "description")}
{{ getField(workshop, "description") }}
</p>
<div class="flex flex-wrap gap-7">
<div class="flex flex-col gap-1">
<span class="font-dm-mono text-xs uppercase tracking-wide text-gray-500">
{$text["details"]?.mentor}
{{ text.details?.mentor }}
</span>
<span class="text-sm text-gray-700 font-space-grotesk">
{getField(workshop, "mentor")}
{{ getField(workshop, "mentor") }}
</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-dm-mono text-xs uppercase tracking-wide text-gray-500">
{$text["details"]?.duration}
{{ text.details?.duration }}
</span>
<span class="text-sm text-gray-700 font-space-grotesk">
{getField(workshop, "duration")}
{{ getField(workshop, "duration") }}
</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-dm-mono text-xs uppercase tracking-wide text-gray-500">
{$text["details"]?.date}
{{ text.details?.date }}
</span>
<span class="text-sm text-gray-700 font-space-grotesk">
{getField(workshop, "date")}
{{ getField(workshop, "date") }}
</span>
</div>
</div>
</div>
</div>
{/each}
</template>
</Stack>
</Section>
</div>
</template>

View File

@@ -1,17 +1,17 @@
// ADD PAGES HERE TO REGISTER ROUTES
// examples: '/page', '/page/:id'
const loadHome = () => import('./Home.svelte');
const loadStudent = () => import('./Student.svelte');
const loadMentors = () => import('./Mentors.svelte');
const loadHelpdesk = () => import('./Helpdesk.svelte');
const loadOurWork = () => import('./OurWork.svelte');
const loadAboutUs = () => import('./AboutUs.svelte');
const loadContact = () => import('./Contact.svelte');
const loadManagment = () => import('./Managment.svelte');
const loadStriim = () => import('./Striim.svelte');
const loadCalendar = () => import('./Calendar.svelte');
const loadWorkshops = () => import('./Workshops.svelte');
const loadHome = () => import('./Home.vue');
const loadStudent = () => import('./Student.vue');
const loadMentors = () => import('./Mentors.vue');
const loadHelpdesk = () => import('./Helpdesk.vue');
const loadOurWork = () => import('./OurWork.vue');
const loadAboutUs = () => import('./AboutUs.vue');
const loadContact = () => import('./Contact.vue');
const loadManagment = () => import('./Managment.vue');
const loadStriim = () => import('./Striim.vue');
const loadCalendar = () => import('./Calendar.vue');
const loadWorkshops = () => import('./Workshops.vue');
// Route mapping for language switching
export const routeMap = {
@@ -110,4 +110,4 @@ export const routes = {
'/striim': loadStriim,
'/kalender': loadCalendar,
'/calendar': loadCalendar,
}
}