diff --git a/package-lock.json b/package-lock.json index 9de7943..c77d294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "homepage", "version": "0.0.1", + "dependencies": { + "zod": "^3.24.1" + }, "devDependencies": { "@eslint/compat": "^1.2.3", "@sveltejs/adapter-node": "^5.2.9", @@ -3583,6 +3586,15 @@ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "dev": true, "license": "MIT" + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 48cc9bf..7d255c4 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,8 @@ "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", "vite": "^5.4.11" + }, + "dependencies": { + "zod": "^3.24.1" } } diff --git a/src/app.html b/src/app.html index 77a5ff5..9730d82 100644 --- a/src/app.html +++ b/src/app.html @@ -7,6 +7,13 @@ %sveltekit.head% +
%sveltekit.body%
diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..15f5023 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,19 @@ +import { building, dev } from '$app/environment'; + +export const handle = async ({ event, resolve }) => { + if (dev || building) return resolve(event); + + const auth = event.request.headers.get("Authorization"); + + if (auth !== `Basic ${btoa('efstajas:super-secret')}`) { + return new Response("Not authorized", { + status: 401, + headers: { + "WWW-Authenticate": + 'Basic realm="User Visible Realm", charset="UTF-8"', + }, + }); + } + + return resolve(event); +}; diff --git a/src/lib/components/header.svelte b/src/lib/components/header.svelte new file mode 100644 index 0000000..7358102 --- /dev/null +++ b/src/lib/components/header.svelte @@ -0,0 +1,123 @@ + + +

Jason Efstathiou

+
+

I’m a freelance software developer and designer from Berlin, Germany.

+
+ + + + + diff --git a/src/lib/components/post-card.svelte b/src/lib/components/post-card.svelte new file mode 100644 index 0000000..872426e --- /dev/null +++ b/src/lib/components/post-card.svelte @@ -0,0 +1,20 @@ + + + + +

{post.title}

+

{post.subtitle}

+
+ + diff --git a/src/lib/components/post-illustration.svelte b/src/lib/components/post-illustration.svelte new file mode 100644 index 0000000..9bfe6ae --- /dev/null +++ b/src/lib/components/post-illustration.svelte @@ -0,0 +1,31 @@ + + +
+ + + +
diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/state/theme/theme.css b/src/lib/state/theme/theme.css new file mode 100644 index 0000000..5b17534 --- /dev/null +++ b/src/lib/state/theme/theme.css @@ -0,0 +1,9 @@ +[data-theme="light"] { + --color-background: white; + --color-foreground: #202020; +} + +[data-theme="dark"] { + --color-background: #202020; + --color-foreground: white; +} diff --git a/src/lib/state/theme/theme.svelte.ts b/src/lib/state/theme/theme.svelte.ts new file mode 100644 index 0000000..9f2013b --- /dev/null +++ b/src/lib/state/theme/theme.svelte.ts @@ -0,0 +1,26 @@ +import { browser } from '$app/environment'; + +type Theme = 'light' | 'dark'; + +type ThemeStore = { + current: Theme; +}; + +export function getTheme() { + return (browser && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; +} + +if (browser) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + theme.current = getTheme(); + }); +} + +export const theme = $state({ current: getTheme() }); + +$effect.root(() => { + $effect(() => { + document.body?.setAttribute('data-theme', theme.current); + console.log('theme changed', theme.current); + }); +}); diff --git a/src/lib/utils/assert.ts b/src/lib/utils/assert.ts new file mode 100644 index 0000000..13abe31 --- /dev/null +++ b/src/lib/utils/assert.ts @@ -0,0 +1,11 @@ +/** + * Asserts that `condition` is truthy. + * @param condition The condition to assert. + * @param message Error message to throw on assertion error. + * @throw An error if `condition` is falsy. + */ +export default function assert(condition: unknown, message = 'Assertion Error'): asserts condition { + if (!condition) { + throw new Error(message); + } +} diff --git a/src/lib/utils/blog.ts b/src/lib/utils/blog.ts new file mode 100644 index 0000000..7de5f4a --- /dev/null +++ b/src/lib/utils/blog.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const postMetadataSchema = z.object({ + title: z.string(), + subtitle: z.string(), + date: z.string(), + illustrationPath: z.string(), + illustrationForegroundColor: z.string(), + illustrationBackgroundColor: z.string(), +}); diff --git a/src/posts/home-assistant.md b/src/posts/home-assistant.md new file mode 100644 index 0000000..dbb444a --- /dev/null +++ b/src/posts/home-assistant.md @@ -0,0 +1,12 @@ +--- +title: 'Seven Years of Home Assistant' +subtitle: 'foo bar' +date: '2024-09-30' +illustrationPath: 'M817 26H544.5M24 446.5V165M420.5 26H544.5M420.5 26V63.25L420.5 85M420.5 26H39.5M24 165H147.5M24 165V41.5M544.5 26L672.54 154.04M420.5 85C411.94 85 405 91.9396 405 100.5C405 104.78 406.735 108.655 409.54 111.46M420.5 85C429.06 85 436 91.9396 436 100.5C436 104.78 434.265 108.655 431.46 111.46M409.54 111.46C412.345 114.265 416.22 116 420.5 116C424.78 116 428.655 114.265 431.46 111.46M409.54 111.46L367.803 153.197M431.46 111.46L485 165H564.5M341 165C341 173.837 348.163 181 357 181C361.164 181 364.956 179.409 367.803 176.803M341 165C341 156.163 348.163 149 357 149C361.164 149 364.956 150.591 367.803 153.197M341 165H178.5M367.803 153.197C370.997 156.123 373 160.327 373 165C373 169.673 370.997 173.878 367.803 176.803M367.803 176.803L460.5 269.5L484.882 293.882M178.5 165C178.5 156.44 171.56 149.5 163 149.5C158.72 149.5 154.845 151.235 152.04 154.04M178.5 165C178.5 169.759 176.355 174.018 172.979 176.861M147.5 165C147.5 173.56 154.44 180.5 163 180.5C166.801 180.5 170.282 179.132 172.979 176.861M147.5 165C147.5 160.72 149.235 156.845 152.04 154.04M152.04 154.04L34.9602 36.9602M39.5 26C39.5 17.4396 32.5604 10.5 24 10.5C15.4396 10.5 8.5 17.4396 8.5 26C8.5 34.5604 15.4396 41.5 24 41.5M39.5 26C39.5 30.2802 37.7651 34.1552 34.9602 36.9602M24 41.5C28.2802 41.5 32.1552 39.7651 34.9602 36.9602M564.5 165C564.5 173.56 571.44 180.5 580 180.5C588.56 180.5 595.5 173.56 595.5 165C595.5 156.44 588.56 149.5 580 149.5C571.44 149.5 564.5 156.44 564.5 165ZM672.54 154.04C669.735 156.845 668 160.72 668 165C668 173.56 674.94 180.5 683.5 180.5C692.06 180.5 699 173.56 699 165C699 156.44 692.06 149.5 683.5 149.5C679.22 149.5 675.345 151.235 672.54 154.04ZM817 293.882H715.441M484.882 293.882H305.5M484.882 293.882H598.5M715.441 293.882L839.059 417.5M715.441 293.882H629.5M305.5 293.882C305.5 302.443 298.56 309.382 290 309.382M305.5 293.882C305.5 285.322 298.56 278.382 290 278.382C285.72 278.382 281.845 280.117 279.04 282.922M290 309.382C281.44 309.382 274.5 302.443 274.5 293.882C274.5 289.602 276.235 285.727 279.04 282.922M290 309.382V426M279.04 282.922L172.979 176.861M629.5 293.882C629.5 302.443 622.56 309.382 614 309.382C605.44 309.382 598.5 302.443 598.5 293.882M629.5 293.882C629.5 285.322 622.56 278.382 614 278.382C609.72 278.382 605.845 280.117 603.04 282.922C603.02 282.942 603.001 282.962 602.981 282.981C600.211 285.781 598.5 289.632 598.5 293.882' +illustrationForegroundColor: '#003A55' +illustrationBackgroundColor: '#87D9FF' +--- + +# Hello + +World! diff --git a/src/posts/homelab.md b/src/posts/homelab.md new file mode 100644 index 0000000..73a321f --- /dev/null +++ b/src/posts/homelab.md @@ -0,0 +1,14 @@ +--- +title: 'Building a miniature data center at home' +subtitle: 'Documenting my homelab journey — from running PiHole to building a distributed, highly available application cluster' +date: '2025-09-30' +illustrationPath: 'M512 125H-24M651 127C651 202.663 589.663 264 514 264C438.337 264 377 202.663 377 127C377 51.337 438.337 -10 514 -10C589.663 -10 651 51.337 651 127ZM798 125.5C798 283.177 670.177 411 512.5 411C354.823 411 227 283.177 227 125.5C227 -32.1773 354.823 -160 512.5 -160C670.177 -160 798 -32.1773 798 125.5ZM602 126.5C602 175.377 562.377 215 513.5 215C464.623 215 425 175.377 425 126.5C425 77.6228 464.623 38 513.5 38C562.377 38 602 77.6228 602 126.5ZM554 126C554 148.644 535.644 167 513 167C490.356 167 472 148.644 472 126C472 103.356 490.356 85 513 85C535.644 85 554 103.356 554 126Z' +illustrationForegroundColor: '#773F00' +illustrationBackgroundColor: '#FFC787' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque2 sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. + +Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. + +Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id. \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..a3f512e --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,36 @@ + + + + + + +
+
+
+ {@render children()} +
+
+ + diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..713818a --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,28 @@ +import assert from '$lib/utils/assert.js'; +import { postMetadataSchema } from '$lib/utils/blog'; + +export const load = async () => { + const posts = await Promise.all( + Object.entries(import.meta.glob('/src/posts/*.md')).map(async ([path, resolver]) => { + const resolved = await resolver(); + + assert(typeof resolved === 'object' && resolved && 'metadata' in resolved); + + const metadata = postMetadataSchema.parse(resolved.metadata); + + const slug = path.split('/').pop()?.slice(0, -3); + + assert(slug); + + return { ...metadata, slug }; + }), + ); + + const sortedPosts = posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + return { + posts: sortedPosts, + }; +}; + +export const prerender = true; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc88df0..1949b85 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,23 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + +
+ {#each data.posts as post} + + +{/each} +
+ + diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte new file mode 100644 index 0000000..2ee9571 --- /dev/null +++ b/src/routes/about/+page.svelte @@ -0,0 +1 @@ +about page \ No newline at end of file diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte new file mode 100644 index 0000000..3f21536 --- /dev/null +++ b/src/routes/blog/[slug]/+page.svelte @@ -0,0 +1,19 @@ + + +
+ + +
+ + diff --git a/src/routes/blog/[slug]/+page.ts b/src/routes/blog/[slug]/+page.ts new file mode 100644 index 0000000..30686f2 --- /dev/null +++ b/src/routes/blog/[slug]/+page.ts @@ -0,0 +1,16 @@ +import { postMetadataSchema } from '$lib/utils/blog.js'; + +export const load = async ({ params }) => { + const { slug } = params; + + const { default: Post, metadata: rawMetadata } = await import(`../../../posts/${slug}.md`); + const metadata = postMetadataSchema.parse(rawMetadata); + + return { + Post, + metadata, + slug, + } +} + +export const prerender = true; diff --git a/src/routes/projects/+page.svelte b/src/routes/projects/+page.svelte new file mode 100644 index 0000000..887eb97 --- /dev/null +++ b/src/routes/projects/+page.svelte @@ -0,0 +1 @@ +projects page \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..a7a5596 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,23 @@ +body { + margin: 0; + font-family: "Suisse Intl", sans-serif; +} + +h1, .h1 { + font-size: 2rem; + font-weight: 400; +} + +p, .p { + font-size: 1rem; + font-weight: 400; + line-height: 1.5rem; +} + +strong { + font-weight: 700; +} + +h1, h2, p { + max-width: 600px; +} diff --git a/svelte.config.js b/svelte.config.js index 6e721a5..a9a6619 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -6,7 +6,9 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; const config = { // Consult https://svelte.dev/docs/kit/integrations // for more information about preprocessors - preprocess: [vitePreprocess(), mdsvex()], + preprocess: [vitePreprocess(), mdsvex({ + extensions: ['.md'], + })], kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. @@ -15,7 +17,7 @@ const config = { adapter: adapter() }, - extensions: ['.svelte', '.svx'] + extensions: ['.svelte', '.md'] }; export default config;