initial stuff

This commit is contained in:
Henri
2025-11-21 08:15:07 +02:00
parent a38a367ac5
commit 8c18827dc5
33 changed files with 522 additions and 148 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>lapikud.github.io</title>
<title>Lapikud</title>
<script>
(function () {
var redirect = sessionStorage.redirect;

30
package-lock.json generated
View File

@@ -7,6 +7,9 @@
"": {
"name": "lapikud.github.io",
"version": "0.0.0",
"dependencies": {
"lucide-svelte": "^0.554.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"svelte": "^5.43.8",
@@ -51,7 +54,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -62,7 +64,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -73,7 +74,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -83,14 +83,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -379,7 +377,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz",
"integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
@@ -440,14 +437,12 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
@@ -461,7 +456,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -471,7 +465,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -481,7 +474,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -529,14 +521,12 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT"
},
"node_modules/esrap": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.3.tgz",
"integrity": "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -579,7 +569,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
@@ -850,14 +839,21 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT"
},
"node_modules/lucide-svelte": {
"version": "0.554.0",
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.554.0.tgz",
"integrity": "sha512-LLcpHi3SuKup0nVD1kKqo8FDZnjXJp48uST26GGh8Jcyrxqk5gmgpnvKmHsHox674UL3cPS1DCul/wFL7ybGqg==",
"license": "ISC",
"peerDependencies": {
"svelte": "^3 || ^4 || ^5.0.0-next.42"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -986,7 +982,6 @@
"version": "5.43.13",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.13.tgz",
"integrity": "sha512-K1QJbpc71OHs3egN/ZHN+nsH/eE/eJQUbtlTAAXVejOOjsFx9hM96fCX5fwPQIYclJfVS8D96RDFA76nZsrJ3w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -1136,7 +1131,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT"
}
}

View File

@@ -15,5 +15,8 @@
},
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
},
"dependencies": {
"lucide-svelte": "^0.554.0"
}
}

28
public/locales/en.json Normal file
View File

@@ -0,0 +1,28 @@
{
"nav": {
"about": "About Us",
"aboutPages": {
"info": "What we're doing?",
"join": "Join Us",
"mentors": "Mentors",
"board": "Board"
},
"events": "Events",
"eventsPages": {
"calendar": "Events Calendar",
"workshops": "Workshops",
"fresh": "Freshmen Initiation",
"repair": "Repair Café"
},
"contact": "Contact",
"helpdesk": "Helpdesk"
},
"content": {},
"footer": {
"address": "Address: ",
"mail": "E-mail: ",
"messenger": "Messenger: ",
"reg": "Reg. code: ",
"toast": "Our socials! Follow us for updates and news."
}
}

28
public/locales/est.json Normal file
View File

@@ -0,0 +1,28 @@
{
"nav": {
"about": "Meist",
"aboutPages": {
"info": "Mis Teeme?",
"join": "Liitu Meiega",
"mentors": "Mentorid",
"board": "Juhatus"
},
"events": "Üritused",
"eventsPages": {
"calendar": "Ürituste Kalender",
"workshops": "Koolitused",
"fresh": "Rebaste Ristimine",
"repair": "Remondikohvik"
},
"contact": "Kontakt",
"helpdesk": "Helpdesk"
},
"content": {},
"footer": {
"address": "Aadress: ",
"mail": "E-post: ",
"messenger": "Messenger: ",
"reg": "Reg. kood: ",
"toast": "Meie sotsiaalid! Jälgi meid uuenduste ja uudiste saamiseks."
}
}

View File

@@ -1,20 +1,12 @@
<script>
import Router from "./lib/Router.svelte";
import { Router, text } from "$lib";
import { routes } from "./routes/index.js";
import { theme, toggle } from "./lib/theme.js";
import Navbar from "./layout/Navbar.svelte";
import Footer from "./layout/Footer.svelte";
</script>
<Navbar />
<Router {routes} />
<Footer />
<!-- use $theme to show current state -->
<button
onclick={toggle}
style="position:fixed;right:1rem;bottom:1rem"
aria-label="Toggle theme"
>
{#if $theme === "dark"}☀️{:else}🌙{/if}
</button>
{#if $text.nav}
<!--<Navbar /> Ära vaata seda faili!! -->
<Router {routes} />
<Footer />
{/if}

View File

@@ -12,12 +12,37 @@ Running `npm run dev` starts a local development server with hot reloading (so y
>
> DO NOT TOUCH unless you know what you're doing! If you want to tinker, then do. When it breaks it just doesn't load any pages
## Internationalization (i18n)
This project uses a simple custom i18n solution defined in `src/lib/i18n.js`.
Import it with `$lib` alias as:
```js
import { currentLang, text, switchLang } from "$lib";
```
text contains a Svelte store that holds the current translations. To use it in a Svelte component, use the `$` prefix to auto-subscribe to the store:
```js
<p>{$text.key}</p> //replace the key with the translation key example: "nav.about"
```
## Styling
Look at `src/app.css` for global styles and CSS variables. Each Svelte component can also have its own styles scoped to that component
Variables are defined in `:root`
we only have 3 colors in out palette + 2 variants:
- `--orange`
- `--black`
- `--white`
- `--off-white`
- `--off-black`
Add new ones only if they seem universal enough to be used in multiple places, don't add stuff like `--space-100: -888px` noone else will ever use that, if you really need it just add it to the component's own style block
## Layout
@@ -39,3 +64,14 @@ There are also helper classes for responsive design:
These classes use media queries defined in `src/app.css` based on common breakpoints.
also things like `.stack`, `.inline`, `.grid`. Go look at `src/app.css` for what they do.
## Routing
Routing is handled by the custom router in `src/lib/Router.svelte` and `src/lib/router/router.js`.
To add new routes, edit `src/routes/index.js` and add your new route there.
Import router functions as:
```js
import { navigate, goBack, reload, getPath, getQuery } from "$lib"; // only import what you need
```

View File

@@ -3,14 +3,11 @@
/* Design tokens & theme variables (light defaults) */
:root {
/* color tokens (light defaults) */
--color-bg: #ffffff;
--color-surface: #f7f8fb;
--color-text: #0f1720;
--color-muted: #6b7280;
--color-primary: #4f46e5;
--color-primary-600: #4338ca;
--color-border: rgba(15, 23, 32, 0.08);
--color-danger: #ef4444;
--orange: #f0941d;
--black: #000000;
--off-black: #0d0d0d;
--white: #ffffff;
--off-white: #fffdfa;
/* layout tokens */
--site-padding: clamp(1rem, 2vw, 2rem); /* horizontal page padding */
@@ -31,7 +28,8 @@
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--shadow-1: 0 1px 2px rgba(2, 6, 23, 0.06), 0 1px 3px rgba(2, 6, 23, 0.04);
--shadow-1: 0 1px 2px rgba(240, 148, 29, 0.2),
0 1px 3px rgba(240, 148, 29, 0.12);
/* breakpoints */
--bp-sm: 480px;
@@ -59,31 +57,6 @@
--accent: var(--color-primary);
}
/* Dark theme overrides: set document.documentElement.dataset.theme = "dark" */
[data-theme="dark"] {
--color-bg: #0b1220;
--color-surface: #0f1724;
--color-text: #e6eef8;
--color-muted: #9aa5b2;
--color-primary: #7c6cff;
--color-primary-600: #6d5cff;
--color-border: rgba(255, 255, 255, 0.06);
--button-bg: #0f1724;
--card-bg: linear-gradient(
180deg,
rgba(18, 20, 28, 0.9),
rgba(15, 17, 24, 0.85)
);
}
/* let the browser apply native form rendering for color-scheme */
:root {
color-scheme: light;
}
[data-theme="dark"] {
color-scheme: dark;
}
/* Global box sizing & accessible defaults */
*,
*::before,

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,20 +1,27 @@
<script>
export let onClick = () => {}
export let onClick = () => {};
</script>
<button on:click={onClick} class="btn">
<button onclick={onClick} class="btn">
<slot />
</button>
<style>
.btn {
background: var(--button-bg);
color: var(--button-text);
background: var(--white);
color: var(--black);
border-radius: 8px;
border: 1px solid transparent;
border: 1.5px solid transparent;
border-color: var(--orange);
padding: 0.6em 1.2em;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
}
.btn:hover {
background: var(--orange);
}
.btn:hover { border-color: var(--primary); }
</style>

View File

@@ -0,0 +1,26 @@
<script>
export let onClick = () => {};
</script>
<button onclick={onClick} class="btn">
<slot />
</button>
<style>
.btn {
background: var(--white);
color: var(--black);
border-radius: 8px;
border: 1.5px solid transparent;
padding: 0.6em 1.2em;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
}
.btn:hover {
background: var(--orange);
}
</style>

View File

@@ -1,8 +0,0 @@
<script>
export let className = '';
export let as = 'div';
</script>
<svelte:component this={as} class={`card ${className}`}>
<slot />
</svelte:component>

View File

@@ -1,9 +0,0 @@
<script>
export let fluid = false; // when true, removes max-width
export let className = '';
export let as = 'div';
</script>
<svelte:component this={as} class={`container ${fluid ? 'container--fluid' : ''} ${className}`}>
<slot />
</svelte:component>

View File

@@ -0,0 +1,71 @@
<script>
import Button from "./Button.svelte";
import ButtonBorderless from "./ButtonBorderless.svelte";
// Props
export let name = "";
export let variant = "default"; // 'default' | 'borderless' aka 'Button' | 'ButtonBorderless'
let isOpen = false;
const toggle = () => (isOpen = !isOpen);
const close = () => (isOpen = false);
function handleFocusOut({ relatedTarget, currentTarget }) {
if (
relatedTarget instanceof HTMLElement &&
currentTarget.contains(relatedTarget)
)
return;
close();
}
</script>
<div class="relative inline-block" on:focusout={handleFocusOut}>
{#if variant === "borderless"}
<ButtonBorderless onClick={toggle}>
{name}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 ml-2 mt-1 transition-transform inline-block"
style:transform={isOpen ? "rotate(180deg)" : "none"}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5"
/>
</svg>
</ButtonBorderless>
{:else}
<Button onClick={toggle}>
{name}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 ml-2 mt-1 transition-transform inline-block"
style:transform={isOpen ? "rotate(180deg)" : "none"}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5"
/>
</svg>
</Button>
{/if}
<div
class="absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black/5 overflow-hidden flex flex-col"
style:visibility={isOpen ? "visible" : "hidden"}
>
<slot />
</div>
</div>

View File

@@ -1,10 +0,0 @@
<script>
export let min = '220px'; // minimum column width
export let gap = 'var(--space-3)';
export let className = '';
export let as = 'div';
</script>
<svelte:component this={as} class={`grid ${className}`} style={`grid-template-columns: repeat(auto-fit, minmax(${min}, 1fr)); gap: ${gap};`}>
<slot />
</svelte:component>

View File

@@ -1,11 +0,0 @@
<script>
// Vertical stack helper. Accepts a gap and alignment so pages can tune spacing.
export let gap = 'var(--space-3)';
export let align = 'stretch';
export let className = '';
export let as = 'div';
</script>
<svelte:component this={as} class={`stack ${className}`} style={`gap: ${gap}; align-items: ${align};`}>
<slot />
</svelte:component>

View File

@@ -1,5 +1,8 @@
export { default as Container } from './Container.svelte'
export { default as Card } from './Card.svelte'
export { default as Stack } from './Stack.svelte'
export { default as Grid } from './Grid.svelte'
export { default as Container } from './layout/Container.svelte'
export { default as Card } from './layout/Card.svelte'
export { default as Stack } from './layout/Stack.svelte'
export { default as Grid } from './layout/Grid.svelte'
export { default as Button } from './Button.svelte'
export { default as ButtonBorderless } from './ButtonBorderless.svelte'
export { default as Dropdown } from './Dropdown.svelte'

View File

@@ -0,0 +1,12 @@
<!--
A simple presentational container that renders a `div` with the `card` class.
-->
<script>
export let className = "";
export let background = "var(--off-white)";
</script>
<div class={`card ${className}`} style="background: {background}">
<slot />
</div>

View File

@@ -0,0 +1,15 @@
<!--
A responsive container that centers content and optionally
removes the max-width when `fluid` is true. Use `className` to
add additional CSS classes to the root element. Content is
projected via the default slot.
-->
<script>
export let fluid = false; // true removes max-width
export let className = "";
</script>
<div class={`container ${fluid ? "container--fluid" : ""} ${className}`}>
<slot />
</div>

View File

@@ -0,0 +1,18 @@
<!--
A responsive grid wrapper using CSS Grid. It exposes `min` and
`gap` props to control the responsive column minimum width and
spacing. Content is projected into the default slot.
-->
<script>
export let min = "220px"; // minimum column width
export let gap = "var(--space-3)";
export let className = "";
</script>
<div
class={`grid ${className}`}
style={`grid-template-columns: repeat(auto-fit, minmax(${min}, 1fr)); gap: ${gap};`}
>
<slot />
</div>

View File

@@ -0,0 +1,13 @@
<!--
A simple stack/column layout that stacks children vertically.
-->
<script>
export let gap = "var(--space-3)";
export let align = "stretch";
export let className = "";
</script>
<div class={`stack ${className}`} style={`gap: ${gap}; align-items: ${align};`}>
<slot />
</div>

16
src/layout/Center.svelte Normal file
View File

@@ -0,0 +1,16 @@
<script>
export let dir = "row"; // 'row' | 'col' (default: 'row')
export let gapClass = "gap-4";
export let className = "";
</script>
<div
class={"flex " +
(dir === "row" ? "flex-row" : "flex-col") +
" items-center justify-center " +
gapClass +
" " +
className}
>
<slot />
</div>

View File

@@ -0,0 +1,65 @@
<script>
import {
Button,
ButtonBorderless,
Dropdown,
Grid,
} from "@/components/index.js";
import Center from "./Center.svelte";
import { navigate, getPath, currentLang, text, switchLang } from "$lib";
// Icon imports (Lucide)
import Coffee from "lucide-svelte/icons/coffee";
import CalendarClock from "lucide-svelte/icons/calendar-clock";
import Presentation from "lucide-svelte/icons/presentation";
import Swords from "lucide-svelte/icons/swords";
import Lectern from "lucide-svelte/icons/lectern";
import Bot from "lucide-svelte/icons/bot";
import HandHeart from "lucide-svelte/icons/hand-heart";
import Trophy from "lucide-svelte/icons/trophy";
</script>
<Grid>
<nav class="w-full h-16">
<Center gapClass="gap-4">
<Button onClick={() => navigate("/")}>Home</Button>
<ButtonBorderless onClick={() => navigate("/about")}
>About</ButtonBorderless
>
<Dropdown name={$text.nav.about} variant="borderless">
<ButtonBorderless onClick={() => navigate("/mis-teeme")}
>{$text.nav.aboutPages.info}</ButtonBorderless
>
<ButtonBorderless onClick={() => navigate("/liitu-meiega")}
><Bot />{$text.nav.aboutPages.join}</ButtonBorderless
>
<ButtonBorderless onClick={() => navigate("/mentorid")}
><HandHeart />{$text.nav.aboutPages.mentors}</ButtonBorderless
>
<ButtonBorderless onClick={() => navigate("/juhatus")}
><Lectern />{$text.nav.aboutPages.board}</ButtonBorderless
>
</Dropdown>
<Dropdown name={$text.nav.events} variant="borderless">
<ButtonBorderless onClick={() => navigate("/kalender")}
><CalendarClock />{$text.nav.eventsPages.calendar}</ButtonBorderless
>
<ButtonBorderless
onClick={() => navigate("https://asikarikas.ee/", { external: true })}
><Trophy />ASI Karikas</ButtonBorderless
>
<ButtonBorderless onClick={() => navigate("/mentorid")}
><Presentation />{$text.nav.eventsPages.workshops}</ButtonBorderless
>
<ButtonBorderless onClick={() => navigate("/rebased")}
><Swords />{$text.nav.eventsPages.fresh}</ButtonBorderless
>
<ButtonBorderless
onClick={() =>
navigate("https://remondikohvik.lapikud.ee/", { external: true })}
><Coffee />{$text.nav.eventsPages.repair}</ButtonBorderless
>
</Dropdown>
</Center>
</nav>
</Grid>

35
src/lib/i18n.js Normal file
View File

@@ -0,0 +1,35 @@
import { writable } from 'svelte/store';
// Load saved language or default to 'est'
const savedLang = typeof window !== 'undefined'
? localStorage.getItem('language') || 'est'
: 'est';
// Store for current language
export const currentLang = writable(savedLang);
// Store for translations
export const text = writable({});
// Load translations for a specific language
async function loadTranslations(lang) {
try {
const response = await fetch(`/locales/${lang}.json`);
const data = await response.json();
text.set(data);
} catch (error) {
console.error(`Failed to load translations for ${lang}:`, error);
}
}
// Initialize translations
loadTranslations(savedLang);
// Function to switch language
export function switchLang(lang) {
currentLang.set(lang);
loadTranslations(lang);
if (typeof window !== 'undefined') {
localStorage.setItem('language', lang);
}
}

4
src/lib/index.js Normal file
View File

@@ -0,0 +1,4 @@
export { navigate, goBack, reload, getPath, getQuery } from './router/router.js';
export { default as Router } from './router/Router.svelte';
export { currentLang, text, switchLang } from './i18n.js';

View File

@@ -2,6 +2,11 @@
<!-- unless the router is broken 👉👈 -->
<script>
export function routeTo(path) {
window.history.pushState({}, "", path);
window.dispatchEvent(new PopStateEvent("popstate"));
}
let { routes } = $props();
let currentPath = $state(window.location.pathname);

44
src/lib/router/router.js Normal file
View File

@@ -0,0 +1,44 @@
/**
* Navigate to a path (internal or external)
* @param {string} path - The path or URL to navigate to
* @param {boolean} external - Whether this is an external link (default: false)
*/
export function navigate(path, external = false) {
if (external) {
window.location.href = path;
} else {
window.history.pushState({}, "", path);
window.dispatchEvent(new PopStateEvent('popstate'));
}
}
/**
* Go back in browser history
*/
export function goBack() {
window.history.back();
}
/**
* Reload the current page
*/
export function reload() {
window.location.reload();
}
/**
* Get the current pathname
* @returns {string} Current path
*/
export function getPath() {
return window.location.pathname;
}
/**
* Get URL query parameters as an object
* @returns {Object} Query parameters
*/
export function getQuery() {
const params = new URLSearchParams(window.location.search);
return Object.fromEntries(params.entries());
}

View File

@@ -1,22 +0,0 @@
// THEME STORE for LIGHT / DARK mode
import { writable } from 'svelte/store'
const init = (() => {
try {
const stored = localStorage.getItem('theme')
if (stored === 'light' || stored === 'dark') return stored
} catch (e) { }
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'
})()
export const theme = writable(init)
theme.subscribe(value => {
try { localStorage.setItem('theme', value) } catch (e) { }
document.documentElement.dataset.theme = value
})
export function toggle() {
theme.update(t => t === 'dark' ? 'light' : 'dark')
}

View File

@@ -1,2 +0,0 @@
<h1>Demo Page</h1>
<a href="/">Back to Home</a>

View File

@@ -1,5 +1,47 @@
<h1>Home</h1>
<nav>
<a href="/demo">Demo</a>
<a href="/contact">Contact</a>
</nav>
<script>
import {
Button,
ButtonBorderless,
Container,
Card,
Stack,
Grid,
} from "@/components/index.js";
</script>
<Container>
<Stack gap="var(--space-6)">
<Card>
<h1>Kes me oleme?</h1>
<p>
Organisatsioon loodi 2002. aastal informaatika tudengite poolt, kelle
erialatunnuseks oli “LAP”. Targad mehed otsustasid, et järelikult on nad
“Lapikud”.
</p>
<Button href="/docs/getting-started">Get Started</Button>
<ButtonBorderless href="/docs/components"
>Explore Components</ButtonBorderless
>
</Card>
<Grid min="250px" gap="var(--space-4)">
<Card>
<h2>Mis on HELPDESK?</h2>
<p>
HELPDESK on MTÜ Lapikute poolt pakutav arvutiabiteenus. Teenus on
suunatud tudengitele, õppejõududele ning kõikidele huvilistele ka
väljaspool Tallinna Tehnikaülikooli. Meie meeskond koosneb kogenud ja
nutikatest Tallinna Tehnikaülikooli IT-tudengitest.
</p>
</Card>
<Card>
<h2>Mis teenuseid osutame?</h2>
<p>
Arvuti riistvara diagnostika ja remont Arvuti riistvara hooldus
Tarkvara diagnostika ja seadistamine Tarkvara paigaldamine Muu
elektroonika parandamine
</p>
</Card>
</Grid>
</Stack>
</Container>

1
src/routes/Test.svelte Normal file
View File

@@ -0,0 +1 @@
<div>testing</div>

View File

@@ -1,10 +1,10 @@
// ADD PAGES HERE TO REGISTER ROUTES
// examples: '/page', '/page/:id'
import Demo from './Demo.svelte'
import Home from './Home.svelte'
import Test from './Test.svelte'
export const routes = {
'/': Home,
'/demo': Demo,
'/juhatus': Test,
}

View File

@@ -8,4 +8,10 @@ export default defineConfig({
server: {
historyApiFallback: true,
},
resolve: {
alias: {
'@': '/src',
'$lib': '/src/lib'
}
}
})