diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..361c983 --- /dev/null +++ b/biome.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.3.3/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off" + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 76e8a9e..2663eff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "website-frontend", "version": "0.0.1", "dependencies": { + "moment": "^2.30.1", + "qs": "^6.12.2", "sanitize.css": "^13.0.0" }, "devDependencies": { @@ -15,6 +17,7 @@ "@sveltejs/adapter-node": "^5.2.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/qs": "^6.9.15", "mdsvex": "^0.11.2", "sass": "^1.77.6", "svelte": "^4.2.7", @@ -970,6 +973,12 @@ "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1110,6 +1119,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1239,6 +1266,22 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1275,6 +1318,25 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -1386,7 +1448,24 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1436,17 +1515,60 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -1712,6 +1834,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -1763,6 +1893,17 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1890,6 +2031,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.2.tgz", + "integrity": "sha512-x+NLUpx9SYrcwXtX7ob1gnkSems4i/mGZX5SlYxwIau6RrUSODO89TR/XDGGpn5RPWSYIB+aSfuSlV5+CmbTBg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2019,6 +2174,22 @@ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "dev": true }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2040,6 +2211,23 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", diff --git a/package.json b/package.json index 99c978e..a0b78cd 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@sveltejs/adapter-node": "^5.2.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/qs": "^6.9.15", "mdsvex": "^0.11.2", "sass": "^1.77.6", "svelte": "^4.2.7", @@ -24,6 +25,8 @@ }, "type": "module", "dependencies": { + "moment": "^2.30.1", + "qs": "^6.12.2", "sanitize.css": "^13.0.0" } } diff --git a/src/components/atoms/Box.svelte b/src/components/atoms/Box.svelte index 8fc07b3..2ced4a1 100644 --- a/src/components/atoms/Box.svelte +++ b/src/components/atoms/Box.svelte @@ -12,7 +12,7 @@ @layer component { .box { padding: 0.5em; - border: 1px solid var(--box-color); + border: 2px solid var(--box-color); border-radius: 10px; margin: auto; height: auto; diff --git a/src/components/molecules/Error.svelte b/src/components/molecules/Error.svelte new file mode 100644 index 0000000..1305464 --- /dev/null +++ b/src/components/molecules/Error.svelte @@ -0,0 +1,38 @@ + + + +
+

{error.message}

+ {#if error.code} + Code: {error.code} + {/if} + Reload! +
+
+ + diff --git a/src/components/molecules/Navigation.svelte b/src/components/molecules/Navigation.svelte index 710e4d4..6307d60 100644 --- a/src/components/molecules/Navigation.svelte +++ b/src/components/molecules/Navigation.svelte @@ -4,6 +4,7 @@ diff --git a/src/components/organisms/BlogPostTeaser.svelte b/src/components/organisms/BlogPostTeaser.svelte new file mode 100644 index 0000000..7a1b020 --- /dev/null +++ b/src/components/organisms/BlogPostTeaser.svelte @@ -0,0 +1,95 @@ + + + + + {@const collection = post.attributes.collection.data?.attributes} + {@const author = post.attributes.author.data?.attributes} + {@const tags = post.attributes.tags.data?.map((d) => d.attributes)} + {@const teaserImage = post.attributes.teaserImage.data.attributes} + {@const imageData = { + altText: teaserImage.alternativeText, + formats: [teaserImage.formats.small, teaserImage.formats.thumbnail], + }} + +
+
+ {#if collection} +
+ in + {collection.name} + +
+ {/if} +

{post.attributes.title}

+ by {author?.name}, + {formatDateRelative(post.attributes.publishedAt)} + {#if tags.length > 0} +
+ + {/if} +
+ {#if post.attributes.teaserImage?.data} +
+ +
+ {/if} +
+ + + + diff --git a/src/global.scss b/src/global.scss index a5dbed9..001ea82 100644 --- a/src/global.scss +++ b/src/global.scss @@ -26,6 +26,11 @@ display: flex; flex-direction: column; } + + .unstyled-link { + text-decoration: inherit; + color: inherit; + } } @font-face { diff --git a/src/lib/cms/blog.ts b/src/lib/cms/blog.ts new file mode 100644 index 0000000..5905732 --- /dev/null +++ b/src/lib/cms/blog.ts @@ -0,0 +1,48 @@ +import type { LinkData, StrapiImage } from "."; +import fetchApi from "./client"; + +export type BlogPostTeaser = { + id: number; + attributes: { + title: string; + createdAt: string; + updatedAt: string; + publishedAt: string; + content: string; + slug: string; + author: { + data?: LinkData; + }; + collection: { + data?: LinkData; + }; + tags: { + data: LinkData[]; + }; + teaserImage: { data: StrapiImage }; + }; +}; + +export async function getPosts(locale = "all"): Promise { + return await fetchApi({ + endpoint: "blog-posts", + wrappedByKey: "data", + query: { + populate: { + author: { + populate: ["slug", "name"], + }, + collection: { + populate: ["slug", "name"], + }, + tags: { + populate: ["slug", "name"], + }, + teaserImage: { + populate: "*", + }, + }, + locale, + }, + }); +} diff --git a/src/lib/cms/client.ts b/src/lib/cms/client.ts new file mode 100644 index 0000000..cc7586c --- /dev/null +++ b/src/lib/cms/client.ts @@ -0,0 +1,51 @@ +import qs from "qs"; +import { STRAPI_CMS_API_KEY, STRAPI_CMS_URL } from "$env/static/private"; + +interface Props { + endpoint: string; + query?: Record; + wrappedByKey?: string; + wrappedByList?: boolean; +} + +/** + * Fetches data from the Strapi API + * @param endpoint - The endpoint to fetch from + * @param query - The query parameters to add to the url + * @param wrappedByKey - The key to unwrap the response from + * @param wrappedByList - If the response is a list, unwrap it + * @returns + */ +export default async function fetchApi({ + endpoint, + query, + wrappedByKey, + wrappedByList, +}: Props): Promise { + if (endpoint.startsWith("/")) { + endpoint = endpoint.slice(1); + } + + const url = new URL(`${STRAPI_CMS_URL}/api/${endpoint}`); + + if (query) { + url.search = qs.stringify(query); + } + console.log({ url }); + const res = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${STRAPI_CMS_API_KEY}`, + }, + }); + let data = await res.json(); + + if (wrappedByKey) { + data = data[wrappedByKey]; + } + + if (wrappedByList) { + data = data[0]; + } + + return data as T; +} diff --git a/src/lib/cms/index.ts b/src/lib/cms/index.ts new file mode 100644 index 0000000..1c54e2d --- /dev/null +++ b/src/lib/cms/index.ts @@ -0,0 +1,44 @@ +export * as blog from "./blog"; + +export type StrapiImage = { + id: number; + attributes: { + name: string; + alternativeText?: string; + caption?: string; + width: number; + height: number; + formats: { + large: ImageFormat; + medium: ImageFormat; + small: ImageFormat; + thumbnail: ImageFormat; + }; + previewUrl?: string; + provider?: string; + provider_metadata?: string; + createdAt: string; + updatedAt: string; + } & ImageFormat; +}; + +export type ImageFormat = { + ext: string; + url: string; + hash: string; + mime: string; + name: string; + path?: string; + size: number; + width: number; + height: number; + sizeInBytes: number; +}; + +export type LinkData = { + id: number; + attributes: { + slug: string; + name: string; + }; +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index 856f2b6..46d3082 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,29 @@ // place files you want to import through the `$lib` alias in this folder. + +import moment from "moment"; + +export function formatDateAbsolute(date: Date | string): string { + return moment(date).format("DD.MM.YYYY. HH:mm:ss"); +} + +export function formatDateRelative(date: Date | string): string { + return moment(date).fromNow(); +} + +export type ImageMetadata = { + caption?: string; + altText?: string; + + formats: { + ext: string; + url: string; + hash: string; + mime: string; + name: string; + path?: string; + size: number; + width: number; + height: number; + sizeInBytes: number; + }[]; +}; diff --git a/src/lib/vars.scss b/src/lib/vars.scss index 7095c38..ecd64fc 100644 --- a/src/lib/vars.scss +++ b/src/lib/vars.scss @@ -22,7 +22,7 @@ } @mixin landscape() { - @media (min-aspect-ratio: 16/10) { + @media (min-aspect-ratio: 16/11) { @content; } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5514e82..0d80758 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -9,7 +9,7 @@

- Welcome to my website. I'm a software developer and tinkerer from + Heyyy. I'm a software developer and tinkerer from Germany. I do a lot of stuff so this website is an attempt in providing an overview.

diff --git a/src/routes/blog/+page.server.ts b/src/routes/blog/+page.server.ts new file mode 100644 index 0000000..6cd3194 --- /dev/null +++ b/src/routes/blog/+page.server.ts @@ -0,0 +1,22 @@ +import { getPosts } from "$lib/cms/blog"; +import { error } from "@sveltejs/kit"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ params }: { params: unknown }) => { + try { + const posts = await getPosts(); + + return { + posts, + }; + } catch (err: any) { + console.error(err); + return { + posts: [], + error: { + message: "Could not load any blog posts :(", + code: 500, + }, + }; + } +}; diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte new file mode 100644 index 0000000..761c4e2 --- /dev/null +++ b/src/routes/blog/+page.svelte @@ -0,0 +1,17 @@ + + +

Blog

+ +{#each data.posts ?? [] as post} + +{/each} + +{#if data.error} + +{/if} diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/uploads/[...path]/+server.ts b/src/routes/uploads/[...path]/+server.ts new file mode 100644 index 0000000..35e976f --- /dev/null +++ b/src/routes/uploads/[...path]/+server.ts @@ -0,0 +1,15 @@ +import { STRAPI_CMS_URL } from "$env/static/private"; + +export async function GET({ + params, +}: { params: { path: string } }): Promise { + const path = params.path; + + try { + return fetch(`${STRAPI_CMS_URL}/uploads/${path}`); + } catch (err) { + return new Response(`File not found: ${path}`, { + status: 404, + }); + } +}