Add blog post teasers

main
trivernis 3 months ago
parent 28a6f9aec9
commit b178c786f8
Signed by: Trivernis
GPG Key ID: 7E6D18B61C8D2F4B

@ -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"
}
}
}
}

192
package-lock.json generated

@ -8,6 +8,8 @@
"name": "website-frontend", "name": "website-frontend",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"moment": "^2.30.1",
"qs": "^6.12.2",
"sanitize.css": "^13.0.0" "sanitize.css": "^13.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -15,6 +17,7 @@
"@sveltejs/adapter-node": "^5.2.0", "@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/qs": "^6.9.15",
"mdsvex": "^0.11.2", "mdsvex": "^0.11.2",
"sass": "^1.77.6", "sass": "^1.77.6",
"svelte": "^4.2.7", "svelte": "^4.2.7",
@ -970,6 +973,12 @@
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true "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": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@ -1110,6 +1119,24 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -1239,6 +1266,22 @@
"node": ">=0.10.0" "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": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -1275,6 +1318,25 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "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": { "node_modules/es6-promise": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@ -1386,7 +1448,24 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "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": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -1436,17 +1515,60 @@
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true "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": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true "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": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@ -1712,6 +1834,14 @@
"mkdirp": "bin/cmd.js" "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": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@ -1763,6 +1893,17 @@
"node": ">=0.10.0" "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": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -1890,6 +2031,20 @@
"node": ">=6" "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": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -2019,6 +2174,22 @@
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==",
"dev": true "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2040,6 +2211,23 @@
"node": ">=8" "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": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",

@ -14,6 +14,7 @@
"@sveltejs/adapter-node": "^5.2.0", "@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/qs": "^6.9.15",
"mdsvex": "^0.11.2", "mdsvex": "^0.11.2",
"sass": "^1.77.6", "sass": "^1.77.6",
"svelte": "^4.2.7", "svelte": "^4.2.7",
@ -24,6 +25,8 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"moment": "^2.30.1",
"qs": "^6.12.2",
"sanitize.css": "^13.0.0" "sanitize.css": "^13.0.0"
} }
} }

@ -12,7 +12,7 @@
@layer component { @layer component {
.box { .box {
padding: 0.5em; padding: 0.5em;
border: 1px solid var(--box-color); border: 2px solid var(--box-color);
border-radius: 10px; border-radius: 10px;
margin: auto; margin: auto;
height: auto; height: auto;

@ -0,0 +1,38 @@
<script lang="ts">
import "$lib/vars.scss";
import { onMount } from "svelte";
import Box from "../atoms/Box.svelte";
export let error: {
message: string;
code?: number;
};
let url = "";
onMount(() => {
url = window.location.href;
})
</script>
<Box color="red">
<div class="error-container">
<h3>{error.message}<span class="spacer"></span></h3>
{#if error.code}
<span>Code: <code>{error.code}</code></span>
{/if}
<b><a href={url} class="reload">Reload!</a></b>
</div>
</Box>
<style lang="scss">
@layer component {
.error-container {
width: 100%;
color: var(--color-red);
.reload {
float: right;
margin-right: 0.5em;
}
}
}
</style>

@ -4,6 +4,7 @@
<nav class="navigation"> <nav class="navigation">
<a class="nav-item" href="/">Home</a> <a class="nav-item" href="/">Home</a>
<a class="nav-item" href="/blog">Blog</a>
</nav> </nav>
<style lang="scss"> <style lang="scss">

@ -0,0 +1,39 @@
<script lang="ts">
import type { ImageMetadata } from "$lib";
import "$lib/vars.scss";
export let imageData: ImageMetadata | undefined = undefined;
</script>
<div class="thumbnail">
{#if imageData}
<picture>
{#each imageData.formats as format}
<source media={`(min-width: ${format.width * 2}px)`} type={format.mime} srcset={format.url} />
{/each}
<img src={imageData.formats[0]?.url} alt={imageData.altText} />
</picture>
{/if}
</div>
<style lang="scss">
@layer component {
.thumbnail {
height: 100%;
width: 100%;
}
picture {
height: 100%;
width: 100%;
display: flex;
overflow: hidden;
img {
width: 100%;
height: auto;
object-fit: cover;
border-radius: 10px;
}
}
}
</style>

@ -0,0 +1,95 @@
<script lang="ts">
import "$lib/vars.scss";
import type { BlogPostTeaser } from "$lib/cms/blog";
import Box from "../atoms/Box.svelte";
import { formatDateAbsolute, formatDateRelative } from "$lib";
import Thumbnail from "../molecules/Thumbnail.svelte";
export let post: BlogPostTeaser;
</script>
<a class="unstyled-link" href={`/blog/${post.attributes.slug}`}>
<Box>
{@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],
}}
<div
class="blog-post-teaser"
class:image-teaser={post.attributes.teaserImage?.data}
>
<div class="teaser-text">
{#if collection}
<h5 class="collection-title">
in <a href={`/blog/collection/${collection.slug}`}>
{collection.name}
</a>
</h5>
{/if}
<h3>{post.attributes.title}</h3>
<span>by {author?.name}</span>,
<span title={formatDateAbsolute(post.attributes.publishedAt)}
>{formatDateRelative(post.attributes.publishedAt)}</span
>
{#if tags.length > 0}
<div class="tag-padding" />
<div class="post-tags">
{#each tags as tag}
<span class="tag"
><a href={`/blog/tag/${tag.slug}`}>{tag.name}</a></span
>
{/each}
</div>
{/if}
</div>
{#if post.attributes.teaserImage?.data}
<div class="teaser-image">
<Thumbnail {imageData} />
</div>
{/if}
</div>
</Box>
</a>
<style lang="scss">
@layer component {
.blog-post-teaser {
position: relative;
.collection-title {
margin: 0;
margin-top: 0.5em;
}
.tag-padding {
height: 1.5em;
}
.post-tags {
position: absolute;
bottom: 0;
.tag {
margin-right: 0.5em;
}
}
}
.image-teaser {
display: flex;
flex-direction: row;
.teaser-text {
display: block;
width: 65%;
}
.teaser-image {
display: block;
width: 35%;
}
}
}
</style>

@ -26,6 +26,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.unstyled-link {
text-decoration: inherit;
color: inherit;
}
} }
@font-face { @font-face {

@ -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<BlogPostTeaser[]> {
return await fetchApi<BlogPostTeaser[]>({
endpoint: "blog-posts",
wrappedByKey: "data",
query: {
populate: {
author: {
populate: ["slug", "name"],
},
collection: {
populate: ["slug", "name"],
},
tags: {
populate: ["slug", "name"],
},
teaserImage: {
populate: "*",
},
},
locale,
},
});
}

@ -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<string, unknown>;
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<T>({
endpoint,
query,
wrappedByKey,
wrappedByList,
}: Props): Promise<T> {
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;
}

@ -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;
};
};

@ -1 +1,29 @@
// place files you want to import through the `$lib` alias in this folder. // 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;
}[];
};

@ -22,7 +22,7 @@
} }
@mixin landscape() { @mixin landscape() {
@media (min-aspect-ratio: 16/10) { @media (min-aspect-ratio: 16/11) {
@content; @content;
} }
} }

@ -9,7 +9,7 @@
<div class="flex-row text-and-image"> <div class="flex-row text-and-image">
<div class="text"> <div class="text">
<p> <p>
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 Germany. I do a lot of stuff so this website is an attempt in providing
an overview. an overview.
</p> </p>

@ -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,
},
};
}
};

@ -0,0 +1,17 @@
<script lang="ts">
import type { PageData } from "../$types";
import Error from "../../components/molecules/Error.svelte";
import BlogPostTeaser from "../../components/organisms/BlogPostTeaser.svelte";
export let data: PageData;
</script>
<h1>Blog</h1>
{#each data.posts ?? [] as post}
<BlogPostTeaser {post} />
{/each}
{#if data.error}
<Error error={data.error} />
{/if}

@ -0,0 +1,15 @@
import { STRAPI_CMS_URL } from "$env/static/private";
export async function GET({
params,
}: { params: { path: string } }): Promise<Response> {
const path = params.path;
try {
return fetch(`${STRAPI_CMS_URL}/uploads/${path}`);
} catch (err) {
return new Response(`File not found: ${path}`, {
status: 404,
});
}
}
Loading…
Cancel
Save