This commit is contained in:
Moritz Hölting 2026-03-17 21:59:15 +01:00
parent 78b5162670
commit dde76774ac
49 changed files with 2415 additions and 2870 deletions

View File

@ -1,34 +1,34 @@
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind";
import solidJs from "@astrojs/solid-js";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import matomo from "./src/integrations/matomo";
import tailwindcss from "@tailwindcss/vite";
import icon from "astro-icon";
// https://astro.build/config
export default defineConfig({
site: "https://hoelting.dev",
trailingSlash: "always",
integrations: [
mdx(),
sitemap(),
solidJs(),
tailwind({ applyBaseStyles: false }),
matomo({
enabled: import.meta.env.PROD, // only enable in production
url: "https://analytics.hoelting.dev",
siteId: 3,
disableCookies: true,
enableCrossDomainLinking: true,
domains: ["*.hoelting.dev", "*.www.hoelting.dev"],
respectDoNotTrack: true,
}),
],
markdown: {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
},
site: "https://hoelting.dev",
trailingSlash: "always",
integrations: [mdx(), sitemap(), icon(), solidJs(), matomo({
enabled: import.meta.env.PROD, // only enable in production
url: "https://analytics.hoelting.dev",
siteId: 3,
disableCookies: true,
enableCrossDomainLinking: true,
domains: ["*.hoelting.dev", "*.www.hoelting.dev"],
respectDoNotTrack: true,
})],
markdown: {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
},
vite: {
plugins: [tailwindcss({ applyBaseStyles: false })],
},
});

View File

@ -15,23 +15,31 @@
},
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/mdx": "^4.3.13",
"@astrojs/rss": "^4.0.14",
"@astrojs/mdx": "^5.0.1",
"@astrojs/rss": "^4.0.17",
"@astrojs/sitemap": "^3.6.0",
"@astrojs/solid-js": "^5.1.3",
"@astrojs/tailwind": "^6.0.2",
"@astrojs/solid-js": "^6.0.0",
"@iconify-icon/solid": "^3.0.3",
"@iconify-json/devicon": "^1.2.61",
"@iconify-json/material-icon-theme": "^1.2.56",
"@iconify-json/pixel": "^1.2.1",
"@iconify-json/pixelarticons": "^1.2.4",
"@tailwindcss/typography": "^0.5.19",
"astro": "^5.16.5",
"astro-og-canvas": "^0.7.2",
"@tailwindcss/vite": "^4.2.1",
"astro": "^6.0.5",
"astro-icon": "^1.1.5",
"astro-og-canvas": "^0.10.1",
"canvaskit-wasm": "^0.40.0",
"clsx": "^2.1.1",
"fuse.js": "^7.1.0",
"iconify-icon": "^3.0.2",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"sharp": "^0.33.5",
"sharp": "^0.34.5",
"solid-js": "^1.9.10",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3"
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
{
"m.server": "matrix.hoelting.dev:443"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,11 @@
function animate() {
const animateElements = document.querySelectorAll('.animate')
const animateElements = document.querySelectorAll('.animate')
animateElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('show')
}, index * 150)
});
animateElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add('show')
}, index * 150)
});
}
document.addEventListener("DOMContentLoaded", animate)

View File

@ -1,96 +0,0 @@
function generateParticles(n) {
let value = `${getRandom(2560)}px ${getRandom(2560)}px #000`;
for (let i = 2; i <= n; i++) {
value += `, ${getRandom(2560)}px ${getRandom(2560)}px #000`;
}
return value;
}
function generateStars(n) {
let value = `${getRandom(2560)}px ${getRandom(2560)}px #fff`;
for (let i = 2; i <= n; i++) {
value += `, ${getRandom(2560)}px ${getRandom(2560)}px #fff`;
}
return value;
}
function getRandom(max) {
return Math.floor(Math.random() * max);
}
function initBG() {
const particlesSmall = generateParticles(1000);
const particlesMedium = generateParticles(500);
const particlesLarge = generateParticles(250);
const particles1 = document.getElementById('particles1');
const particles2 = document.getElementById('particles2');
const particles3 = document.getElementById('particles3');
if (particles1) {
particles1.style.cssText = `
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: ${particlesSmall};
animation: animStar 50s linear infinite;
`;
}
if (particles2) {
particles2.style.cssText = `
width: 1.5px;
height: 1.5px;
border-radius: 50%;
box-shadow: ${particlesMedium};
animation: animateParticle 100s linear infinite;
`;
}
if (particles3) {
particles3.style.cssText = `
width: 2px;
height: 2px;
border-radius: 50%;
box-shadow: ${particlesLarge};
animation: animateParticle 150s linear infinite;
`;
}
const starsSmall = generateStars(1000);
const starsMedium = generateStars(500);
const starsLarge = generateStars(250);
const stars1 = document.getElementById('stars1');
const stars2 = document.getElementById('stars2');
const stars3 = document.getElementById('stars3');
if (stars1) {
stars1.style.cssText = `
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: ${starsSmall};
`;
}
if (stars2) {
stars2.style.cssText = `
width: 1.5px;
height: 1.5px;
border-radius: 50%;
box-shadow: ${starsMedium};
`;
}
if (stars3) {
stars3.style.cssText = `
width: 2px;
height: 2px;
border-radius: 50%;
box-shadow: ${starsLarge};
`;
}
}
document.addEventListener('astro:after-swap', initBG);
initBG();

108
public/rss-style.xsl Normal file
View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="UTF-8" />
<xsl:template match="/">
<html>
<head>
<style>
@font-face {
font-family: "DepartureMono";
src: url("/fonts/DepartureMono-Regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "FiraCode";
src: url("/fonts/FiraCode-Regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "FiraCode";
src: url("/fonts/FiraCode-Bold.woff") format("woff");
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: "FiraCode", monospace;
background: #1a1a1a;
color: #ccc;
image-rendering: pixelated;
max-width: 768px;
margin-inline: auto;
}
* {
box-sizing: border-box;
}
h1, h2 {
font-family: "DepartureMono", monospace;
}
#backContainer {
margin-block: 1rem;
}
#backButton {
color: #ccc;
text-decoration: none;
padding: 0.5rem 1rem;
background: #555;
border-radius: calc(infinity * 1px);;
}
.item {
border: 2px solid #ccc;
padding: 1rem;
margin-bottom: 1rem;
}
.item > h2 > a {
color: #ccc;
text-decoration: none;
}
.item > .pills {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 1rem;
}
.item > .pills > * {
color: #ccc;
text-decoration: none;
text-transform: capitalize;
background: #555;
padding: 0.5rem 1rem;
border-radius: calc(infinity * 1px);;
}
h1, h2 {
margin: 0 0 1rem 0;
}
</style>
</head>
<body>
<h1><xsl:value-of select="rss/channel/title" /></h1>
<div id="backContainer">
<a id="backButton" href="/">Back to the main page</a>
</div>
<xsl:for-each select="rss/channel/item">
<div class="item">
<h2><a href="{link}"><xsl:value-of select="title" /></a></h2>
<div class="pills">
<a class="type" href="/{entryType}/"><xsl:value-of select="entryType" /></a>
<span class="date"><xsl:value-of select="pubDate" /></span>
</div>
<p><xsl:value-of select="description" /></p>
</div>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View File

@ -1,16 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="email" viewBox="0 0 512 512">
<path d="M64 112c-8.8 0-16 7.2-16 16v22.1L220.5 291.7c20.7 17 50.4 17 71.1 0L464 150.1V128c0-8.8-7.2-16-16-16H64zM48 212.2V384c0 8.8 7.2 16 16 16H448c8.8 0 16-7.2 16-16V212.2L322 328.8c-38.4 31.5-93.7 31.5-132 0L48 212.2zM0 128C0 92.7 28.7 64 64 64H448c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/>
</symbol>
<symbol id="github" viewBox="0 0 496 512">
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/>
</symbol>
<symbol id="linkedin" viewBox="0 0 448 512">
<path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/>
</symbol>
<symbol id="twitter-x" viewBox="0 0 512 512">
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- astro icon -->
<symbol id="astro" viewBox="0 0 85 107">
<path d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" fill="#F041FF"/>
<path d="M0 69.5866C0 69.5866 14.3139 62.6137 28.6678 62.6137L39.4901 29.1204C39.8953 27.5007 41.0783 26.3999 42.4139 26.3999C43.7495 26.3999 44.9325 27.5007 45.3377 29.1204L56.1601 62.6137C73.1601 62.6137 84.8278 69.5866 84.8278 69.5866C84.8278 69.5866 60.5145 3.35233 60.467 3.21944C59.7692 1.2612 58.5911 0 57.0029 0H27.8274C26.2392 0 25.1087 1.2612 24.3634 3.21944C24.3108 3.34983 0 69.5866 0 69.5866Z" fill="currentColor"/>
</symbol>
<!-- javascript icon -->
<symbol id="javascript" viewBox="0 0 630 630">
<rect width="630" height="630" fill="#f7df1e"/>
<path fill="black" d="m423.2 492.19c12.69 20.72 29.2 35.95 58.4 35.95 24.53 0 40.2-12.26 40.2-29.2 0-20.3-16.1-27.49-43.1-39.3l-14.8-6.35c-42.72-18.2-71.1-41-71.1-89.2 0-44.4 33.83-78.2 86.7-78.2 37.64 0 64.7 13.1 84.2 47.4l-46.1 29.6c-10.15-18.2-21.1-25.37-38.1-25.37-17.34 0-28.33 11-28.33 25.37 0 17.76 11 24.95 36.4 35.95l14.8 6.34c50.3 21.57 78.7 43.56 78.7 93 0 53.3-41.87 82.5-98.1 82.5-54.98 0-90.5-26.2-107.88-60.54zm-209.13 5.13c9.3 16.5 17.76 30.45 38.1 30.45 19.45 0 31.72-7.61 31.72-37.2v-201.3h59.2v202.1c0 61.3-35.94 89.2-88.4 89.2-47.4 0-74.85-24.53-88.81-54.075z"/>
</symbol>
<!-- typescript icon -->
<symbol id="typescript" fill="none" viewBox="0 0 512 512">
<rect fill="#3178c6" height="512" rx="50" width="512"/>
<rect fill="#3178c6" height="512" rx="50" width="512"/>
<path clip-rule="evenodd" fill="#fff" fill-rule="evenodd" d="m316.939 407.424v50.061c8.138 4.172 17.763 7.3 28.875 9.386s22.823 3.129 35.135 3.129c11.999 0 23.397-1.147 34.196-3.442 10.799-2.294 20.268-6.075 28.406-11.342 8.138-5.266 14.581-12.15 19.328-20.65s7.121-19.007 7.121-31.522c0-9.074-1.356-17.026-4.069-23.857s-6.625-12.906-11.738-18.225c-5.112-5.319-11.242-10.091-18.389-14.315s-15.207-8.213-24.18-11.967c-6.573-2.712-12.468-5.345-17.685-7.9-5.217-2.556-9.651-5.163-13.303-7.822-3.652-2.66-6.469-5.476-8.451-8.448-1.982-2.973-2.974-6.336-2.974-10.091 0-3.441.887-6.544 2.661-9.308s4.278-5.136 7.512-7.118c3.235-1.981 7.199-3.52 11.894-4.615 4.696-1.095 9.912-1.642 15.651-1.642 4.173 0 8.581.313 13.224.938 4.643.626 9.312 1.591 14.008 2.894 4.695 1.304 9.259 2.947 13.694 4.928 4.434 1.982 8.529 4.276 12.285 6.884v-46.776c-7.616-2.92-15.937-5.084-24.962-6.492s-19.381-2.112-31.066-2.112c-11.895 0-23.163 1.278-33.805 3.833s-20.006 6.544-28.093 11.967c-8.086 5.424-14.476 12.333-19.171 20.729-4.695 8.395-7.043 18.433-7.043 30.114 0 14.914 4.304 27.638 12.912 38.172 8.607 10.533 21.675 19.45 39.204 26.751 6.886 2.816 13.303 5.579 19.25 8.291s11.086 5.528 15.415 8.448c4.33 2.92 7.747 6.101 10.252 9.543 2.504 3.441 3.756 7.352 3.756 11.733 0 3.233-.783 6.231-2.348 8.995s-3.939 5.162-7.121 7.196-7.147 3.624-11.894 4.771c-4.748 1.148-10.303 1.721-16.668 1.721-10.851 0-21.597-1.903-32.24-5.71-10.642-3.806-20.502-9.516-29.579-17.13zm-84.159-123.342h64.22v-41.082h-179v41.082h63.906v182.918h50.874z"/>
</symbol>
<!-- tailwind icon -->
<symbol id="tailwind" viewBox="0 0 47 40" fill="none">
<path fill="#6366f1" d="M23.5 6.5C17.5 6.5 13.75 9.5 12.25 15.5C14.5 12.5 17.125 11.375 20.125 12.125C21.8367 12.5529 23.0601 13.7947 24.4142 15.1692C26.6202 17.4084 29.1734 20 34.75 20C40.75 20 44.5 17 46 11C43.75 14 41.125 15.125 38.125 14.375C36.4133 13.9471 35.1899 12.7053 33.8357 11.3308C31.6297 9.09158 29.0766 6.5 23.5 6.5ZM12.25 20C6.25 20 2.5 23 1 29C3.25 26 5.875 24.875 8.875 25.625C10.5867 26.0529 11.8101 27.2947 13.1642 28.6693C15.3702 30.9084 17.9234 33.5 23.5 33.5C29.5 33.5 33.25 30.5 34.75 24.5C32.5 27.5 29.875 28.625 26.875 27.875C25.1633 27.4471 23.9399 26.2053 22.5858 24.8307C20.3798 22.5916 17.8266 20 12.25 20Z"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,87 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- menu -->
<symbol id="menu" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M3 12h18"/>
<path d="M3 6h18"/>
<path d="M3 18h18"/>
</symbol>
<!-- x -->
<symbol id="x" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M18 6L6 18"/>
<path d="M6 6l12 12"/>
</symbol>
<!-- search -->
<symbol id="search" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M11 17.25a6.25 6.25 0 110-12.5 6.25 6.25 0 010 12.5z"/>
<path d="M16 16l4.5 4.5"/>
</symbol>
<!-- rss -->
<symbol id="rss" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M4 11a9 9 0 019 9"/>
<path d="M4 4a16 16 0 0116 16"/>
<circle cx="5" cy="19" r="1"/>
</symbol>
<!-- sun -->
<symbol id="sun" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2"/>
<path d="M12 21v2"/>
<path d="M4.22 4.22l1.42 1.42"/>
<path d="M18.36 18.36l1.42 1.42"/>
<path d="M1 12h2"/>
<path d="M21 12h2"/>
<path d="M4.22 19.78l1.42-1.42"/>
<path d="M18.36 5.64l1.42-1.42"/>
</symbol>
<!-- moon -->
<symbol id="moon" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</symbol>
<!-- square -->
<symbol id="square" viewBox="0 0 448 512">
<path d="M384 80c8.8 0 16 7.2 16 16V416c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16H384zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/>
</symbol>
<!-- square-check -->
<symbol id="square-check" viewBox="0 0 448 512">
<path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM337 209L209 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L303 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
</symbol>
<!-- calendar -->
<symbol id="calendar" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<path d="M16 2v4"/>
<path d="M8 2v4"/>
<path d="M3 10h18"/>
</symbol>
<!-- book-open -->
<symbol id="book-open" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/>
<path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/>
</symbol>
<!-- arrow-right -->
<symbol id="arrow-right" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M5 12h14"/>
<path d="M12 5l7 7-7 7"/>
</symbol>
<!-- globe -->
<symbol id="globe" viewBox="0 0 16 16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.268 14.0934C11.9051 13.4838 13.2303 12.2333 13.9384 10.6469C13.1192 10.7941 12.2138 10.9111 11.2469 10.9925C11.0336 12.2005 10.695 13.2621 10.268 14.0934ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.48347 14.4823C8.32384 14.494 8.16262 14.5 8 14.5C7.83738 14.5 7.67616 14.494 7.51654 14.4823C7.5132 14.4791 7.50984 14.4759 7.50647 14.4726C7.2415 14.2165 6.94578 13.7854 6.67032 13.1558C6.41594 12.5744 6.19979 11.8714 6.04101 11.0778C6.67605 11.1088 7.33104 11.125 8 11.125C8.66896 11.125 9.32395 11.1088 9.95899 11.0778C9.80021 11.8714 9.58406 12.5744 9.32968 13.1558C9.05422 13.7854 8.7585 14.2165 8.49353 14.4726C8.49016 14.4759 8.4868 14.4791 8.48347 14.4823ZM11.4187 9.72246C12.5137 9.62096 13.5116 9.47245 14.3724 9.28806C14.4561 8.87172 14.5 8.44099 14.5 8C14.5 7.55901 14.4561 7.12828 14.3724 6.71194C13.5116 6.52755 12.5137 6.37904 11.4187 6.27753C11.4719 6.83232 11.5 7.40867 11.5 8C11.5 8.59133 11.4719 9.16768 11.4187 9.72246ZM10.1525 6.18401C10.2157 6.75982 10.25 7.36805 10.25 8C10.25 8.63195 10.2157 9.24018 10.1525 9.81598C9.46123 9.85455 8.7409 9.875 8 9.875C7.25909 9.875 6.53877 9.85455 5.84749 9.81598C5.7843 9.24018 5.75 8.63195 5.75 8C5.75 7.36805 5.7843 6.75982 5.84749 6.18401C6.53877 6.14545 7.25909 6.125 8 6.125C8.74091 6.125 9.46123 6.14545 10.1525 6.18401ZM11.2469 5.00748C12.2138 5.08891 13.1191 5.20593 13.9384 5.35306C13.2303 3.7667 11.9051 2.51622 10.268 1.90662C10.695 2.73788 11.0336 3.79953 11.2469 5.00748ZM8.48347 1.51771C8.4868 1.52089 8.49016 1.52411 8.49353 1.52737C8.7585 1.78353 9.05422 2.21456 9.32968 2.84417C9.58406 3.42562 9.80021 4.12856 9.95899 4.92219C9.32395 4.89118 8.66896 4.875 8 4.875C7.33104 4.875 6.67605 4.89118 6.04101 4.92219C6.19978 4.12856 6.41594 3.42562 6.67032 2.84417C6.94578 2.21456 7.2415 1.78353 7.50647 1.52737C7.50984 1.52411 7.51319 1.52089 7.51653 1.51771C7.67615 1.50597 7.83738 1.5 8 1.5C8.16262 1.5 8.32384 1.50597 8.48347 1.51771ZM5.73202 1.90663C4.0949 2.51622 2.76975 3.7667 2.06159 5.35306C2.88085 5.20593 3.78617 5.08891 4.75309 5.00748C4.96639 3.79953 5.30497 2.73788 5.73202 1.90663ZM4.58133 6.27753C3.48633 6.37904 2.48837 6.52755 1.62761 6.71194C1.54392 7.12828 1.5 7.55901 1.5 8C1.5 8.44099 1.54392 8.87172 1.62761 9.28806C2.48837 9.47245 3.48633 9.62096 4.58133 9.72246C4.52807 9.16768 4.5 8.59133 4.5 8C4.5 7.40867 4.52807 6.83232 4.58133 6.27753ZM4.75309 10.9925C3.78617 10.9111 2.88085 10.7941 2.06159 10.6469C2.76975 12.2333 4.0949 13.4838 5.73202 14.0934C5.30497 13.2621 4.96639 12.2005 4.75309 10.9925Z"/>
</symbol>
<!-- link -->
<symbol id="link" viewBox="0 0 16 16">
<path d="M8.46968 1.46968C10.1433 -0.203925 12.8567 -0.203923 14.5303 1.46968C16.2039 3.14329 16.2039 5.85674 14.5303 7.53034L12.0303 10.0303L10.9697 8.96968L13.4697 6.46968C14.5575 5.38186 14.5575 3.61816 13.4697 2.53034C12.3819 1.44252 10.6182 1.44252 9.53034 2.53034L7.03034 5.03034L5.96968 3.96968L8.46968 1.46968ZM11.5303 5.53034L5.53034 11.5303L4.46968 10.4697L10.4697 4.46968L11.5303 5.53034ZM1.46968 14.5303C3.14329 16.2039 5.85673 16.204 7.53034 14.5303L10.0303 12.0303L8.96968 10.9697L6.46968 13.4697C5.38186 14.5575 3.61816 14.5575 2.53034 13.4697C1.44252 12.3819 1.44252 10.6182 2.53034 9.53034L5.03034 7.03034L3.96968 5.96968L1.46968 8.46968C-0.203923 10.1433 -0.203925 12.8567 1.46968 14.5303Z"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -1,25 +1,27 @@
import { formatDate } from "@lib/utils";
import type { CollectionEntry } from "astro:content";
import { formatDate } from "@/lib/utils";
import { Icon } from '@iconify-icon/solid';
type Props = {
entry: CollectionEntry<"blog"> | CollectionEntry<"projects">;
pill?: boolean;
pill?: string;
};
export default function ArrowCard({ entry, pill }: Props) {
return (
<a
href={`/${entry.collection}/${entry.slug}/`}
href={`/${entry.collection}/${entry.id}/`}
class="group p-4 gap-3 flex items-center border rounded-lg hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out"
>
<div class="w-full group-hover:text-black group-hover:dark:text-white blend">
<div class="flex flex-wrap items-center gap-2">
{pill && (
{pill != undefined && (
<div class="text-sm capitalize px-2 py-0.5 rounded-full border border-black/15 dark:border-white/25">
{entry.collection === "blog" ? "post" : "project"}
{pill}
</div>
)}
<div class="text-sm uppercase">
<div class="text-sm font-departure uppercase">
{formatDate(entry.data.date)}
</div>
</div>
@ -30,39 +32,28 @@ export default function ArrowCard({ entry, pill }: Props) {
<div class="text-sm line-clamp-2">{entry.data.summary}</div>
<ul class="flex flex-wrap mt-2 gap-1">
{entry.data.tags.map(
(
tag: string // this line has an error; Parameter 'tag' implicitly has an 'any' type.ts(7006)
) => (
<li class="text-xs uppercase py-0.5 px-1 rounded bg-black/5 dark:bg-white/20 text-black/75 dark:text-white/75">
(tag) => (
<li class="text-xs font-departure uppercase py-0.5 px-1 rounded bg-black/5 dark:bg-white/20 text-black/75 dark:text-white/75">
{tag}
</li>
)
)}
</ul>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="scale-x-0 group-hover:scale-x-100 translate-x-4 group-hover:translate-x-1 transition-all duration-300 ease-in-out"
<div class="relative overflow-hidden w-4 h-4">
<Icon
icon="pixelarticons:arrow-right"
class="absolute right-0 inset-y-0 text-lg origin-left
scale-x-0 group-hover:scale-x-100
transition-all duration-300 ease-in-out"
/>
<polyline
points="12 5 19 12 12 19"
class="translate-x-0 group-hover:translate-x-1 transition-all duration-300 ease-in-out"
<Icon
icon="pixelarticons:chevron-right"
class="absolute right-0 inset-y-0 text-lg
group-hover:translate-x-4
transition-all duration-300 ease-in-out"
/>
</svg>
</div>
</a>
);
}

View File

@ -0,0 +1,117 @@
---
import { render, type CollectionEntry } from "astro:content";
import Container from "./Container.astro";
type Props = {
entry: CollectionEntry<"projects"> | CollectionEntry<"blog">;
};
const { entry } = Astro.props;
const { Content, headings } = await render(entry);
---
<div class="article-layout">
<!-- Sidebar -->
<aside
class="animate toc font-departure max-w-3xl mx-auto px-5 xl:fixed xl:top-24 xl:right-[max(2rem,calc((100vw-84rem)/2))] xl:h-max xl:w-66"
>
<h3 class="font-semibold text-[0.9rem] opacity-70 mb-2">
On this page
</h3>
<ul class="list-none p-0 m-0">
{
headings.map((h) => (
<li class={`level-${h.depth}`}>
<a href={`#${h.slug}`}>{h.text}</a>
</li>
))
}
</ul>
</aside>
<!-- Main content -->
<Container size="md">
<article class="animate content">
<Content />
</article>
</Container>
</div>
<style>
.toc li {
margin-bottom: 0.4rem;
}
.toc li.level-2 {
margin-left: 0.5rem;
}
.toc li.level-3 {
margin-left: 1rem;
}
.toc a {
opacity: 0.7;
text-decoration: none;
transition: opacity 0.15s ease;
}
.toc a.active {
opacity: 1;
font-weight: 600;
}
.toc a:hover {
opacity: 1;
}
article.content {
grid-area: content;
}
</style>
<style is:global>
article.content :is(h1, h2, h3, h4, h5, h6) {
font-family: "DepartureMono", monospace;
scroll-margin-top: 6rem;
}
article.content a {
font-family: "DepartureMono", monospace;
text-decoration: none;
}
article.content a:hover {
text-decoration: underline;
}
</style>
<script>
const headings = document.querySelectorAll(
"article.content h1[id], article.content h2[id], article.content h3[id]",
);
const tocLinks = document.querySelectorAll(".toc a");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute("id");
tocLinks.forEach((link) => {
link.classList.toggle(
"active",
link.getAttribute("href") === `#${id}`,
);
});
}
});
},
{
rootMargin: "0px 0px -80% 0px",
threshold: 0.1,
},
);
headings.forEach((h) => observer.observe(h));
</script>

View File

@ -20,14 +20,21 @@ const { title, description, image = "/open-graph.jpg" } = Astro.props;
<link
rel="preload"
href="/fonts/atkinson-regular.woff"
href="/fonts/DepartureMono-Regular.woff"
as="font"
type="font/woff"
crossorigin
/>
<link
rel="preload"
href="/fonts/atkinson-bold.woff"
href="/fonts/FiraCode-Regular.woff"
as="font"
type="font/woff"
crossorigin
/>
<link
rel="preload"
href="/fonts/FiraCode-Bold.woff"
as="font"
type="font/woff"
crossorigin
@ -86,10 +93,14 @@ const { title, description, image = "/open-graph.jpg" } = Astro.props;
...(
e as TransitionBeforeSwapEvent
).newDocument.head.querySelectorAll('link[as="font"]'),
].forEach((link) => link.remove())
].forEach((link) => link.remove()),
);
</script>
<script>
import "iconify-icon";
</script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/katex.css"

View File

@ -1,7 +1,8 @@
import type { CollectionEntry } from "astro:content";
import { createEffect, createSignal, For, Show } from "solid-js";
import ArrowCard from "@components/ArrowCard";
import { cn } from "@lib/utils";
import ArrowCard from "@/components/ArrowCard";
import { cn } from "@/lib/utils";
import { Icon } from "@iconify-icon/solid";
type Props = {
tags: string[];
@ -40,7 +41,7 @@ export default function Blog({ data, tags }: Props) {
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-1">
<div class="sticky top-24">
<div class="text-sm font-semibold uppercase mb-2 text-black dark:text-white">
<div class="text-sm font-semibold font-departure uppercase mb-2 text-black dark:text-white">
Filter
</div>
<ul class="flex flex-wrap sm:flex-col gap-1.5">
@ -50,41 +51,29 @@ export default function Blog({ data, tags }: Props) {
<button
onClick={() => toggleTag(tag)}
class={cn(
"w-full px-2 py-1 rounded",
"w-full px-2 py-1 rounded font-departure",
"whitespace-nowrap overflow-hidden overflow-ellipsis",
"flex gap-2 items-center",
"bg-black/5 dark:bg-white/10",
"hover:bg-black/10 hover:dark:bg-white/15",
"transition-colors duration-300 ease-in-out",
filter().has(tag) &&
"text-black dark:text-white"
"text-black dark:text-white"
)}
>
<svg
class={cn(
"size-5 fill-black/50 dark:fill-white/50",
"transition-colors duration-300 ease-in-out",
filter().has(tag) &&
"fill-black dark:fill-white"
)}
>
<use
href={`/ui.svg#square`}
class={cn(
!filter().has(tag)
? "block"
: "hidden"
)}
/>
<use
href={`/ui.svg#square-check`}
class={cn(
filter().has(tag)
? "block"
: "hidden"
)}
/>
</svg>
<div class={cn(
"relative size-5 fill-black/50 dark:fill-white/50",
"transition-colors duration-300 ease-in-out",
filter().has(tag) &&
"fill-black dark:fill-white"
)}>
<Icon icon="pixelarticons:square" />
<Icon icon="pixel:check" class={cn("absolute top-0 right-0",
filter().has(tag)
? "block"
: "hidden"
)} />
</div>
{tag}
</button>
</li>
@ -95,7 +84,7 @@ export default function Blog({ data, tags }: Props) {
</div>
<div class="col-span-3 sm:col-span-2">
<div class="flex flex-col">
<div class="text-sm uppercase mb-2">
<div class="text-sm font-departure uppercase mb-2">
SHOWING {posts().length} OF {data.length} POSTS
</div>
<ul class="flex flex-col gap-3">
@ -106,7 +95,7 @@ export default function Blog({ data, tags }: Props) {
))}
<Show when={posts().length == 0}>
<h3 class="mt-5 text-2xl">
<h3 class="mt-5 text-2xl font-departure">
There seem to be no blogs matching the filters
yet...
</h3>

View File

@ -1,20 +1,24 @@
---
import { cn } from "@lib/utils"
import { cn } from "@/lib/utils";
type Props = {
size: "sm" | "md" | "lg" | "xl" | "2xl"
}
size: "sm" | "md" | "lg" | "xl" | "2xl";
class?: string;
};
const { size } = Astro.props;
const { size, class: className } = Astro.props;
---
<div class={cn(
"w-full h-full mx-auto px-5",
size === "sm" && "max-w-screen-sm",
size === "md" && "max-w-screen-md",
size === "lg" && "max-w-screen-lg",
size === "xl" && "max-w-screen-xl",
size === "2xl" && "max-w-screen-2xl",
)}>
<slot/>
<div
class={cn(
"w-full h-full mx-auto px-5",
size === "sm" && "max-w-screen-sm",
size === "md" && "max-w-3xl",
size === "lg" && "max-w-5xl",
size === "xl" && "max-w-7xl",
size === "2xl" && "max-w-screen-2xl",
className,
)}
>
<slot />
</div>

View File

@ -1,6 +1,6 @@
---
import { SITE, LINKS } from "@consts";
import { cn } from "@lib/utils";
import { SITE, LINKS } from "@/consts";
import { cn } from "@/lib/utils";
const { pathname } = Astro.url;
const subpath = pathname.match(/[^/]+/g);
---
@ -16,13 +16,13 @@ const subpath = pathname.match(/[^/]+/g);
href={LINK.HREF}
class={cn(
"flex items-center justify-center px-3 py-1 rounded-full",
"text-current hover:text-black dark:hover:text-white",
"text-current font-departure hover:text-black dark:hover:text-white",
"hover:bg-black/5 dark:hover:bg-white/20",
"transition-colors duration-300 ease-in-out",
pathname === LINK.HREF ||
"/" + subpath?.[0] === LINK.HREF
? "pointer-events-none bg-black dark:bg-white text-white dark:text-black"
: ""
: "",
)}
>
{LINK.TEXT}
@ -36,39 +36,35 @@ const subpath = pathname.match(/[^/]+/g);
href="/search/"
aria-label={`Search blog posts and projects on ${SITE.TITLE}`}
class={cn(
"size-9 rounded-full p-2 items-center justify-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out",
pathname === "/search" || "/" + subpath?.[0] === "search"
"size-9 rounded-full p-2 items-center justify-center text-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out",
pathname === "/search/" || "/" + subpath?.[0] === "search"
? "pointer-events-none bg-black dark:bg-white text-white dark:text-black"
: ""
: "",
)}
>
<svg class="size-full">
<use href="/ui.svg#search"></use>
</svg>
<div class="text-2xl -translate-y-2">&#xf002</div>
</a>
<a
href="/rss.xml"
target="_blank"
aria-label={`Rss feed for ${SITE.TITLE}`}
class="size-9 rounded-full p-2 items-center justify-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out"
class="size-9 rounded-full p-2 items-center justify-center text-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out"
>
<svg class="size-full">
<use href="/ui.svg#rss"></use>
</svg>
<div class="text-2xl -translate-y-2">&#xf428</div>
</a>
<button
id="drawer-theme-button"
aria-label={`Toggle light and dark theme`}
class="size-9 rounded-full p-2 items-center justify-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out"
class="size-9 rounded-full p-2 items-center justify-center text-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out"
>
<svg class="block dark:hidden size-full">
<use href="/ui.svg#sun"></use>
</svg>
<svg class="hidden dark:block size-full">
<use href="/ui.svg#moon"></use>
</svg>
<span class="block dark:hidden text-2xl -translate-y-2"
>&#xf522</span
>
<span class="hidden dark:block text-2xl -translate-y-2"
>&#xf4ee</span
>
</button>
</div>
</div>

View File

@ -1,48 +1,40 @@
---
import { Image } from "astro:assets";
import { SITE, SOCIALS } from "@consts";
import Container from "@components/Container.astro";
import { Icon } from "astro-icon/components";
import { SITE, SOCIALS } from "@/consts";
import Container from "@/components/Container.astro";
import pictureOfMe from "@images/me.jpg";
import pictureOfMe from "@/images/me.jpg";
const CURRENT_YEAR = new Date().getFullYear();
---
<footer class="relative bg-white dark:bg-black">
<div class="animate">
<footer class="relative">
<div class="animate bg-zinc-200 dark:bg-zinc-950/50">
<section class="py-5">
<Container size="md">
<div class="flex items-center justify-center sm:justify-end">
<button
id="back-to-top"
aria-label="Back to top of page"
class="group flex w-fit p-1.5 gap-1.5 text-sm items-center border rounded hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out"
class="group flex w-fit p-1.5 gap-1.5 text-sm items-center border rounded hover:bg-black/15 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white rotate-90"
>
<line
x1="19"
y1="12"
x2="5"
y2="12"
class="scale-x-0 group-hover:scale-x-100 translate-x-3 group-hover:translate-x-0 transition-all duration-300 ease-in-out"
></line>
<polyline
points="12 19 5 12 12 5"
class="translate-x-1 group-hover:translate-x-0 transition-all duration-300 ease-in-out"
></polyline>
</svg>
<div class="relative overflow-hidden w-5 h-4 -mt-1">
<Icon
name="pixelarticons:arrow-up"
class:list="absolute top-0 inset-x-0 text-lg origin-bottom
scale-y-0 group-hover:scale-y-100
transition-all duration-300 ease-in-out"
/>
<Icon
name="pixelarticons:chevron-up"
class:list="absolute top-0 inset-x-0 text-lg
group-hover:translate-y-4
transition-all duration-300 ease-in-out"
/>
</div>
<div
class="w-full group-hover:text-black group-hover:dark:text-white transition-colors duration-300 ease-in-out"
class="w-full font-departure group-hover:text-black group-hover:dark:text-white transition-colors duration-300 ease-in-out"
>
Back to top
</div>
@ -65,20 +57,18 @@ const CURRENT_YEAR = new Date().getFullYear();
src={pictureOfMe}
loading="lazy"
alt="Me"
class="size-6 fill-current rounded-full"
class="size-6 fill-current rounded-lg mr-2"
quality="low"
/>
{SITE.TITLE}
{SITE.AUTHOR}
</a>
</div>
<div
class="flex gap-2 justify-center sm:justify-end items-center"
class="flex gap-2 justify-center font-departure sm:justify-end items-center"
>
<span class="relative flex h-3 w-3">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-300"
></span>
<span
class="relative inline-flex rounded-full h-3 w-3 bg-green-500"
class="absolute inline-flex h-full w-full rounded-sm bg-green-500"
></span>
</span>
All systems normal
@ -95,7 +85,7 @@ const CURRENT_YEAR = new Date().getFullYear();
<div
class="order-2 sm:order-1 flex flex-col items-center justify-center sm:items-start"
>
<div class="text-sm mt-2">
<div class="text-sm font-departure mt-2">
&copy; {CURRENT_YEAR} | All rights reserved
</div>
</div>
@ -112,13 +102,12 @@ const CURRENT_YEAR = new Date().getFullYear();
href={SOCIAL.HREF}
target="_blank"
aria-label={`${SITE.TITLE} on ${SOCIAL.NAME}`}
class="group size-10 rounded-full p-2 items-center justify-center hover:bg-black/5 dark:hover:bg-white/20 blend"
class="group size-10 rounded-full p-2 flex items-center justify-center hover:bg-black/15 dark:hover:bg-white/20 blend"
>
<svg class="size-full fill-current group-hover:fill-black group-hover:dark:fill-white blend">
<use
href={`/social.svg#${SOCIAL.ICON}`}
/>
</svg>
<Icon
name={SOCIAL.ICON}
class:list="text-xl text-center group-hover:fill-black group-hover:dark:fill-white blend"
/>
</a>
))
}

View File

@ -1,10 +1,11 @@
---
import { Image } from "astro:assets";
import { SITE, LINKS } from "@consts";
import { cn } from "@lib/utils";
import Container from "@components/Container.astro";
import { SITE, LINKS } from "@/consts";
import { cn } from "@/lib/utils";
import Container from "@/components/Container.astro";
import { Icon } from "astro-icon/components";
import pictureOfMe from "@images/me.jpg";
import pictureOfMe from "@/images/me.jpg";
const { pathname } = Astro.url;
const subpath = pathname.match(/[^/]+/g);
@ -24,9 +25,10 @@ const subpath = pathname.match(/[^/]+/g);
src={pictureOfMe}
loading="eager"
alt="Me"
class="size-6 fill-current rounded-full"
class="size-6 fill-current rounded-lg mr-2"
quality="low"
/>
<div>
<div class="font-departure font-bold">
{SITE.TITLE}
</div>
</a>
@ -43,13 +45,13 @@ const subpath = pathname.match(/[^/]+/g);
<a
href={LINK.HREF}
class={cn(
"h-8 rounded-full px-3 text-current",
"h-8 rounded-xl px-3 text-current font-departure",
"flex items-center justify-center",
"transition-colors duration-300 ease-in-out",
pathname === LINK.HREF ||
"/" + subpath?.[0] === LINK.HREF
"/" + subpath?.[0] + "/" === LINK.HREF
? "bg-black dark:bg-white text-white dark:text-black"
: "hover:bg-black/5 dark:hover:bg-white/20 hover:text-black dark:hover:text-white"
: "hover:bg-black/5 dark:hover:bg-white/20 hover:text-black dark:hover:text-white",
)}
>
{LINK.TEXT}
@ -63,62 +65,60 @@ const subpath = pathname.match(/[^/]+/g);
class="buttons absolute right-0 top-1/2 -translate-y-1/2 flex gap-1"
>
<a
href="/search"
aria-label={`Search blog posts and projects on ${SITE.TITLE}`}
href="/search/"
aria-label="Search blog posts and projects"
class={cn(
"hidden md:flex",
"size-9 rounded-full p-2 items-center justify-center",
"size-9 rounded-full p-2 items-center justify-center text-center",
"bg-transparent hover:bg-black/5 dark:hover:bg-white/20",
"stroke-current hover:stroke-black hover:dark:stroke-white",
"border border-black/10 dark:border-white/25",
"transition-colors duration-300 ease-in-out",
pathname === "/search" ||
"/" + subpath?.[0] === "/search"
pathname === "/search/" ||
"/" + subpath?.[0] === "/search/"
? "pointer-events-none bg-black dark:bg-white text-white dark:text-black"
: ""
: "",
)}
>
<svg class="size-full">
<use href="/ui.svg#search"></use>
</svg>
<Icon name="pixelarticons:search" class:list="text-xl" />
</a>
<a
href="/rss.xml"
target="_blank"
aria-label={`Rss feed for ${SITE.TITLE}`}
aria-label="Rss feed"
class={cn(
"hidden md:flex",
"size-9 rounded-full p-2 items-center justify-center",
"size-9 rounded-full p-2 items-center justify-center text-center",
"bg-transparent hover:bg-black/5 dark:hover:bg-white/20",
"stroke-current hover:stroke-black hover:dark:stroke-white",
"border border-black/10 dark:border-white/25",
"transition-colors duration-300 ease-in-out"
"transition-colors duration-300 ease-in-out active:opacity-1!",
)}
>
<svg class="size-full">
<use href="/ui.svg#rss"></use>
</svg>
<Icon name="pixelarticons:rss" class:list="text-xl" />
</a>
<button
id="header-theme-button"
aria-label={`Toggle light and dark theme`}
aria-label="Toggle light and dark theme"
class={cn(
"hidden md:flex",
"size-9 rounded-full p-2 items-center justify-center",
"size-9 rounded-full p-2 items-center justify-center text-center",
"bg-transparent hover:bg-black/5 dark:hover:bg-white/20",
"stroke-current hover:stroke-black hover:dark:stroke-white",
"border border-black/10 dark:border-white/25",
"transition-colors duration-300 ease-in-out"
"transition-colors duration-300 ease-in-out",
)}
>
<svg class="size-full block dark:hidden">
<use href="/ui.svg#sun"></use>
</svg>
<svg class="size-full hidden dark:block">
<use href="/ui.svg#moon"></use>
</svg>
<Icon
name="pixelarticons:sun"
class:list={"text-xl block dark:hidden"}
/>
<Icon
name="pixelarticons:moon"
class:list={"text-xl hidden dark:block"}
/>
</button>
<button
@ -126,19 +126,22 @@ const subpath = pathname.match(/[^/]+/g);
aria-label={`Toggle drawer open and closed`}
class={cn(
"flex md:hidden",
"size-9 rounded-full p-2 items-center justify-center",
"size-9 rounded-full p-2 items-center justify-center text-center",
"bg-transparent hover:bg-black/5 dark:hover:bg-white/20",
"stroke-current hover:stroke-black hover:dark:stroke-white",
"border border-black/10 dark:border-white/25",
"transition-colors duration-300 ease-in-out"
"transition-colors duration-300 ease-in-out",
)}
>
<svg id="drawer-open" class="size-full">
<use href="/ui.svg#menu"></use>
</svg>
<svg id="drawer-close" class="size-full">
<use href="/ui.svg#x"></use>
</svg>
<div id="drawer-open">
<Icon name="pixelarticons:menu" class:list="text-xl" />
</div>
<div id="drawer-close">
<Icon
name="pixelarticons:chevron-up"
class:list="text-xl"
/>
</div>
</button>
</div>
</div>

View File

@ -1,40 +0,0 @@
---
/**
* Meteors.astro
* This component creates meteors that are appended to the galaxy on interval.
* Meteors are removed from the document after the animation is completed.
* There are four (4) meteor shower containers, one for each diagonal direction.
*/
---
<div id="meteors">
<!-- rotations defined in base.css & tailwind.config.mjs -->
<div class="shower ur" />
<div class="shower dr" />
<div class="shower dl" />
<div class="shower ul" />
</div>
<script>
function createMeteor () {
// create a meteor
let meteor = document.createElement("div");
meteor.setAttribute("class", "meteor");
meteor.style.left = Math.round(Math.random() * window.innerWidth) + "px";
meteor.style.top = Math.round(Math.random() * window.innerHeight) + "px";
// append the meteor to a random meteor shower (direction)
const showers = document.querySelectorAll(".shower");
const random = Math.floor(Math.random() * showers.length);
const shower = showers[random];
shower.append(meteor);
// remove the meteor after the animation duration
setTimeout(() => {
meteor.remove();
}, 3500);
}
// Create meteors on interval on two (2) seconds
setInterval(createMeteor, 1500);
</script>

View File

@ -1,7 +1,8 @@
import type { CollectionEntry } from "astro:content";
import { createEffect, createSignal, For, Show } from "solid-js";
import ArrowCard from "@components/ArrowCard";
import { cn } from "@lib/utils";
import ArrowCard from "@/components/ArrowCard";
import { cn } from "@/lib/utils";
import { Icon } from "@iconify-icon/solid";
type Props = {
tags: string[];
@ -42,7 +43,7 @@ export default function Projects({ data, tags }: Props) {
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-1">
<div class="sticky top-24">
<div class="text-sm font-semibold uppercase mb-2 text-black dark:text-white">
<div class="text-sm font-semibold font-departure uppercase mb-2 text-black dark:text-white">
Filter
</div>
<ul class="flex flex-wrap sm:flex-col gap-1.5">
@ -52,41 +53,29 @@ export default function Projects({ data, tags }: Props) {
<button
onClick={() => toggleTag(tag)}
class={cn(
"w-full px-2 py-1 rounded",
"w-full px-2 py-1 rounded font-departure",
"whitespace-nowrap overflow-hidden overflow-ellipsis",
"flex gap-2 items-center",
"bg-black/5 dark:bg-white/10",
"hover:bg-black/10 hover:dark:bg-white/15",
"transition-colors duration-300 ease-in-out",
filter().has(tag) &&
"text-black dark:text-white"
"text-black dark:text-white"
)}
>
<svg
class={cn(
"size-5 fill-black/50 dark:fill-white/50",
"transition-colors duration-300 ease-in-out",
filter().has(tag) &&
"fill-black dark:fill-white"
)}
>
<use
href={`/ui.svg#square`}
class={cn(
!filter().has(tag)
? "block"
: "hidden"
)}
/>
<use
href={`/ui.svg#square-check`}
class={cn(
filter().has(tag)
? "block"
: "hidden"
)}
/>
</svg>
<div class={cn(
"relative size-5 fill-black/50 dark:fill-white/50",
"transition-colors duration-300 ease-in-out",
filter().has(tag) &&
"fill-black dark:fill-white"
)}>
<Icon icon="pixelarticons:square" />
<Icon icon="pixel:check" class={cn("absolute top-0 right-0",
filter().has(tag)
? "block"
: "hidden"
)} />
</div>
{tag}
</button>
</li>
@ -97,7 +86,7 @@ export default function Projects({ data, tags }: Props) {
</div>
<div class="col-span-3 sm:col-span-2">
<div class="flex flex-col">
<div class="text-sm uppercase mb-2">
<div class="text-sm font-departure uppercase mb-2">
SHOWING {projects().length} OF {data.length} PROJECTS
</div>
<ul class="flex flex-col gap-3">
@ -107,7 +96,7 @@ export default function Projects({ data, tags }: Props) {
</li>
))}
<Show when={projects().length == 0}>
<h3 class="mt-5 text-2xl">
<h3 class="mt-5 text-2xl font-departure">
There seem to be no projects matching the
filters yet...
</h3>

View File

@ -1,13 +1,13 @@
import type { CollectionEntry } from "astro:content"
import { createEffect, createSignal } from "solid-js"
import Fuse from "fuse.js"
import ArrowCard from "@components/ArrowCard"
import ArrowCard from "@/components/ArrowCard"
type Props = {
data: CollectionEntry<"blog">[]
}
export default function Search({data}: Props) {
export default function Search({ data }: Props) {
const [query, setQuery] = createSignal("")
const [results, setResults] = createSignal<CollectionEntry<"blog">[]>([])
@ -34,20 +34,20 @@ export default function Search({data}: Props) {
return (
<div class="flex flex-col">
<div class="relative">
<input name="search" type="text" value={query()} onInput={onInput} autocomplete="off" spellcheck={false} placeholder="What are you looking for?" class="w-full px-2.5 py-1.5 pl-10 rounded outline-none text-black dark:text-white bg-black/5 dark:bg-white/15 border border-black/10 dark:border-white/20 focus:border-black focus:dark:border-white"/>
<input name="search" type="text" value={query()} onInput={onInput} autocomplete="off" spellcheck={false} placeholder="What are you looking for?" class="w-full px-2.5 py-1.5 pl-10 rounded outline-none text-black dark:text-white bg-black/5 dark:bg-white/15 border border-black/10 dark:border-white/20 focus:border-black focus:dark:border-white" />
<svg class="absolute size-6 left-1.5 top-1/2 -translate-y-1/2 stroke-current">
<use href={`/ui.svg#search`}/>
<use href={`/ui.svg#search`} />
</svg>
</div>
{(query().length >= 2 && results().length >= 1) && (
<div class="mt-12">
<div class="text-sm uppercase mb-2">
<div class="text-sm uppercase mb-2 font-departure">
Found {results().length} results for {`'${query()}'`}
</div>
<ul class="flex flex-col gap-3">
{results().map(result => (
<li>
<ArrowCard entry={result} pill={true} />
<ArrowCard entry={result} pill={result.collection} />
</li>
))}
</ul>

View File

@ -1,18 +1,26 @@
---
import { Icon } from "astro-icon/components";
type Props = {
text: string
icon: string
href: string
}
text: string;
icon: string;
href: string;
};
const { text, icon, href } = Astro.props
const { text, icon, href } = Astro.props;
---
<a href={href} target="_blank" class="w-fit px-3 py-2 group rounded border flex gap-2 items-center border-neutral-200 dark:border-neutral-700 hover:bg-neutral-100 hover:dark:bg-neutral-800 blend">
<svg height={20} width={20}>
<use href={`/stack.svg#${icon}`}></use>
</svg>
<span class="text-sm capitalize text-neutral-500 dark:text-neutral-400 group-hover:text-black group-hover:dark:text-white blend">
{text}
</span>
<a
href={href}
target="_blank"
class="w-fit px-3 py-2 group rounded border flex gap-2 items-center border-neutral-200 dark:border-neutral-700 hover:bg-neutral-100 hover:dark:bg-neutral-800 blend"
>
<div>
<Icon name={icon} class:list="text-xl" />
</div>
<span
class="text-sm capitalize text-neutral-500 dark:text-neutral-400 group-hover:text-black group-hover:dark:text-white blend"
>
{text}
</span>
</a>

View File

@ -1,90 +0,0 @@
---
/**
* TwinkleStars.astro
* This component creates twinkling stars that are appended to the galaxy on interval.
* Twinkle stars are removed from the document after the animation is completed.
* The svg below is just a template for the script to clone and append to the galaxy.
*/
---
<svg
id="twinkle-star"
class="template"
width="149"
height="149"
viewBox="0 0 149 149"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="absolute left-full animate-twinkle"
>
<circle cx="74" cy="74" r="11" fill="white"></circle>
<rect
y="141.421"
width="200"
height="10"
transform="rotate(-45 0 141.421)"
fill="url(#paint0_linear_4_2)"></rect>
<rect
x="7.07107"
width="200"
height="10"
transform="rotate(45 7.07107 0)"
fill="url(#paint1_linear_4_2)"></rect>
<defs>
<linearGradient
id="paint0_linear_4_2"
x1="0"
y1="146.421"
x2="200"
y2="146.421"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1E1E1E"></stop>
<stop offset="0.445" stop-color="white"></stop>
<stop offset="0.58721" stop-color="white"></stop>
<stop offset="1" stop-color="#1E1E1E"></stop>
</linearGradient>
<linearGradient
id="paint1_linear_4_2"
x1="7.07107"
y1="5"
x2="207.071"
y2="5"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1E1E1E"></stop>
<stop offset="0.42" stop-color="white"></stop>
<stop offset="0.555" stop-color="white"></stop>
<stop offset="1" stop-color="#1E1E1E"></stop>
</linearGradient>
</defs>
</svg>
<script is:inline>
// Generate a twinkle star and append it to the galaxy, remove it after animation.
function generateTwinkleStar() {
// Clone the twinkle star template and set its attributes.
const twinkleStar = document
.getElementById("twinkle-star")
?.cloneNode(true);
twinkleStar.style.position = "absolute";
twinkleStar.style.left =
Math.floor(Math.random() * window.innerWidth) + "px";
twinkleStar.style.top =
Math.floor(Math.random() * (window.innerHeight / 3)) + "px";
twinkleStar.style.width =
window.innerWidth < 768
? Math.floor(Math.random() * (15 - 7.5 + 1) + 7.5)
: Math.floor(Math.random() * (30 - 15 + 1) + 15) + "px";
twinkleStar.style.height = twinkleStar.style.width;
twinkleStar.classList.add("twinkle");
document.getElementById("galaxy").appendChild(twinkleStar);
// Remove the twinkle star after the animation is completed.
setTimeout(() => {
twinkleStar.remove();
}, 2500);
}
setInterval(generateTwinkleStar, 5000);
</script>

View File

@ -1,8 +1,8 @@
import type { Site, Page, Links, Socials } from "@types";
import type { Site, Page, Links, Socials } from "@/types";
// Global
export const SITE: Site = {
TITLE: "Portfolio Moritz Hölting",
TITLE: "Portfolio",
DESCRIPTION: "Welcome to my portfolio website.",
AUTHOR: "Moritz Hölting",
};
@ -55,26 +55,20 @@ export const LINKS: Links = [
export const SOCIALS: Socials = [
{
NAME: "Email",
ICON: "email",
ICON: "pixelarticons:at-sign",
TEXT: "moritz@hoelting.dev",
HREF: "mailto:moritz@hoelting.dev",
},
{
NAME: "Github",
ICON: "github",
ICON: "pixel:github",
TEXT: "moritz-hoelting",
HREF: "https://github.com/moritz-hoelting",
},
{
NAME: "LinkedIn",
ICON: "linkedin",
ICON: "pixel:linkedin",
TEXT: "moritz-hölting",
HREF: "https://www.linkedin.com/in/moritz-h%C3%B6lting/",
},
{
NAME: "Twitter",
ICON: "twitter-x",
TEXT: "moritz_hoelting",
HREF: "https://twitter.com/moritz_hoelting",
},
];

View File

@ -1,7 +1,9 @@
import { defineCollection, z } from "astro:content";
import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { glob } from "astro/loaders";
const work = defineCollection({
type: "content",
loader: glob({ base: "./src/content/work", pattern: "**/*.{md,mdx}" }),
schema: z.object({
company: z.string(),
role: z.string(),
@ -11,7 +13,7 @@ const work = defineCollection({
});
const blog = defineCollection({
type: "content",
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: z.object({
title: z.string(),
summary: z.string(),
@ -22,7 +24,7 @@ const blog = defineCollection({
});
const projects = defineCollection({
type: "content",
loader: glob({ base: "./src/content/projects", pattern: "**/*.{md,mdx}" }),
schema: z.object({
title: z.string(),
summary: z.string(),

View File

@ -21,7 +21,7 @@ export type MatomoOptions = {
};
export default function matomoIntegration(
options: MatomoOptions
options: MatomoOptions,
): AstroIntegration {
if (!options?.url.endsWith("/")) {
options.url += "/";
@ -30,8 +30,8 @@ export default function matomoIntegration(
let script: string;
if (options?.enabled) {
script = `import { initMatomo, preconnectMatomo } from "@integrations/matomo/matomo"; initMatomo(${JSON.stringify(
options
script = `import { initMatomo, preconnectMatomo } from "@/integrations/matomo/matomo"; initMatomo(${JSON.stringify(
options,
)});`;
if (options?.preconnect) {
@ -44,7 +44,7 @@ export default function matomoIntegration(
"\x1b[0m",
"\x1b[34m",
"Is disabled.",
"\x1b[0m"
"\x1b[0m",
);
script = "";
@ -62,7 +62,7 @@ export default function matomoIntegration(
"\x1b[0m",
"\x1b[34m",
"was integrated successfully.",
"\x1b[0m"
"\x1b[0m",
);
}
},

View File

@ -1,5 +1,6 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import { Icon } from "astro-icon/components";
type Props = {
entry: CollectionEntry<"blog"> | CollectionEntry<"projects">;
@ -8,95 +9,73 @@ type Props = {
// Get the requested entry
const { entry } = Astro.props;
const { collection } = entry;
const { Content } = await entry.render();
// Get the next and prev entries (modulo to wrap index)
const items = (await getCollection(collection))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const index = items.findIndex((x) => x.slug === entry.slug);
const index = items.findIndex((x) => x.id === entry.id);
const prev = items[(index + 1) % items.length];
const next = items[(index - 1 + items.length) % items.length];
---
<div>
<article>
<Content />
</article>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<a
href={`/${prev.collection}/${prev.slug}/`}
href={`/${prev.collection}/${prev.id}/`}
class="group p-4 gap-3 flex items-center border rounded-lg hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 blend"
>
<div
class="order-2 w-full h-full group-hover:text-black group-hover:dark:text-white blend"
>
<div class="flex flex-wrap gap-2">
<div class="text-sm uppercase">Prev</div>
<div class="text-sm font-departure uppercase">Prev</div>
</div>
<div class="font-semibold mt-3 text-black dark:text-white">
{prev.data.title}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="order-1 stroke-current group-hover:stroke-black group-hover:dark:stroke-white rotate-180"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="scale-x-0 group-hover:scale-x-100 translate-x-4 group-hover:translate-x-1 transition-all duration-300 ease-in-out"
></line>
<polyline
points="12 5 19 12 12 19"
class="translate-x-0 group-hover:translate-x-1 transition-all duration-300 ease-in-out"
></polyline>
</svg>
<div class="relative overflow-hidden w-4 h-4">
<Icon
name="pixelarticons:arrow-left"
class:list="absolute left-0 inset-y-0 text-lg origin-right
scale-x-0 group-hover:scale-x-100
transition-all duration-300 ease-in-out"
/>
<Icon
name="pixelarticons:chevron-left"
class:list="absolute left-0 inset-y-0 text-lg
group-hover:-translate-x-4
transition-all duration-300 ease-in-out"
/>
</div>
</a>
<a
href={`/${next.collection}/${next.slug}/`}
href={`/${next.collection}/${next.id}/`}
class="group p-4 gap-3 flex items-center border rounded-lg hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out"
>
<div
class="w-full h-full text-right group-hover:text-black group-hover:dark:text-white blend"
>
<div class="text-sm uppercase">Next</div>
<div class="text-sm font-departure uppercase">Next</div>
<div class="font-semibold mt-3 text-black dark:text-white">
{next.data.title}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="scale-x-0 group-hover:scale-x-100 translate-x-4 group-hover:translate-x-1 transition-all duration-300 ease-in-out"
></line>
<polyline
points="12 5 19 12 12 19"
class="translate-x-0 group-hover:translate-x-1 transition-all duration-300 ease-in-out"
></polyline>
</svg>
<div class="relative overflow-hidden w-4 h-4">
<Icon
name="pixelarticons:arrow-right"
class:list="absolute right-0 inset-y-0 text-lg origin-left
scale-x-0 group-hover:scale-x-100
transition-all duration-300 ease-in-out"
/>
<Icon
name="pixelarticons:chevron-right"
class:list="absolute right-0 inset-y-0 text-lg
group-hover:translate-x-4
transition-all duration-300 ease-in-out"
/>
</div>
</a>
</div>
</div>

View File

@ -1,6 +1,7 @@
---
import type { CollectionEntry } from "astro:content";
import { formatDate, readingTime } from "@lib/utils";
import { Icon } from "astro-icon/components";
import { formatDate, readingTime } from "@/lib/utils";
type Props = {
entry: CollectionEntry<"projects"> | CollectionEntry<"blog">;
@ -19,50 +20,41 @@ const repoUrl = collection === "projects" ? data.repoUrl : null;
href={`/${collection}/`}
class="group w-fit p-1.5 gap-1.5 text-sm flex items-center border rounded hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white"
>
<line
x1="19"
y1="12"
x2="5"
y2="12"
class="scale-x-0 group-hover:scale-x-100 translate-x-3 group-hover:translate-x-0 transition-all duration-300 ease-in-out"
></line>
<polyline
points="12 19 5 12 12 5"
class="translate-x-1 group-hover:translate-x-0 transition-all duration-300 ease-in-out"
></polyline>
</svg>
<div class="relative overflow-hidden w-4 h-4">
<Icon
name="pixelarticons:arrow-left"
class:list="absolute left-0 inset-y-0 text-lg origin-right
scale-x-0 group-hover:scale-x-100
transition-all duration-300 ease-in-out"
/>
<Icon
name="pixelarticons:chevron-left"
class:list="absolute left-0 inset-y-0 text-lg
group-hover:-translate-x-4
transition-all duration-300 ease-in-out"
/>
</div>
<div
class="w-full group-hover:text-black group-hover:dark:text-white transition-colors duration-300 ease-in-out"
class="w-full font-departure group-hover:text-black group-hover:dark:text-white transition-colors duration-300 ease-in-out"
>
Back to {collection}
</div>
</a>
<div class="flex flex-wrap text-sm uppercase mt-12 gap-3 opacity-75">
<div
class="flex flex-wrap text-sm font-departure uppercase mt-12 gap-3 opacity-75"
>
<div class="flex items-center gap-2">
<svg class="size-5 stroke-current">
<use href="/ui.svg#calendar"></use>
</svg>
<Icon name="pixelarticons:calendar-2" class:list="size-5" />
{formatDate(date)}
</div>
<div class="flex items-center gap-2">
<svg class="size-5 stroke-current">
<use href="/ui.svg#book-open"></use>
</svg>
{readingTime(body)}
<Icon name="pixelarticons:book-open" class:list="size-5" />
{readingTime(body ?? "")}
</div>
</div>
<h1 class="text-3xl font-semibold text-black dark:text-white mt-2">
<h1
class="text-3xl font-semibold font-departure text-black dark:text-white mt-2"
>
{title}
</h1>
<div class="mt-1">
@ -77,13 +69,8 @@ const repoUrl = collection === "projects" ? data.repoUrl : null;
target="_blank"
class="group flex gap-2 items-center px-3 py-1.5 truncate rounded text-xs md:text-sm lg:text-base border border-black/25 dark:border-white/25 hover:bg-black/5 hover:dark:bg-white/15 blend"
>
<svg class="size-4">
<use
href="/ui.svg#globe"
class="fill-current group-hover:fill-black group-hover:dark:fill-white blend"
/>
</svg>
<span class="text-current group-hover:text-black group-hover:dark:text-white blend">
<Icon name="pixelarticons:globe" class:list="size-4" />
<span class="text-current font-departure group-hover:text-black group-hover:dark:text-white blend">
See Demo
</span>
</a>
@ -94,13 +81,8 @@ const repoUrl = collection === "projects" ? data.repoUrl : null;
target="_blank"
class="group flex gap-2 items-center px-3 py-1.5 truncate rounded text-xs md:text-sm lg:text-base border border-black/25 dark:border-white/25 hover:bg-black/5 hover:dark:bg-white/15 blend"
>
<svg class="size-4">
<use
href="/ui.svg#link"
class="fill-current group-hover:fill-black group-hover:dark:fill-white blend"
/>
</svg>
<span class="text-current group-hover:text-black group-hover:dark:text-white blend">
<Icon name="pixelarticons:link" class:list="size-4" />
<span class="text-current font-departure group-hover:text-black group-hover:dark:text-white blend">
See Repository
</span>
</a>

View File

@ -1,5 +1,5 @@
---
import Container from "@components/Container.astro";
import Container from "@/components/Container.astro";
---
<div class="flex-1 py-5">

View File

@ -1,10 +1,10 @@
---
import "@styles/global.css";
import BaseHead from "@components/BaseHead.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Drawer from "@components/Drawer.astro";
import { SITE } from "@consts";
import "@/styles/global.css";
import BaseHead from "@/components/BaseHead.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Drawer from "@/components/Drawer.astro";
import { SITE } from "@/consts";
export interface Props {
title: string;
@ -19,7 +19,7 @@ const { title, description, image } = Astro.props;
<html lang="en">
<head>
<BaseHead
title={`${title} | ${SITE.TITLE}`}
title={`${title} | ${SITE.TITLE} ${SITE.AUTHOR}`}
description={description}
{image}
/>

View File

@ -1,5 +1,5 @@
---
import Container from "@components/Container.astro";
import Container from "@/components/Container.astro";
---
<div class="pt-36 pb-5">

View File

@ -1,10 +1,11 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import ArticleTopLayout from "@layouts/ArticleTopLayout.astro";
import ArticleBottomLayout from "@layouts/ArticleBottomLayout.astro";
import PageLayout from "@/layouts/PageLayout.astro";
import TopLayout from "@/layouts/TopLayout.astro";
import BottomLayout from "@/layouts/BottomLayout.astro";
import ArticleTopLayout from "@/layouts/ArticleTopLayout.astro";
import ArticleBottomLayout from "@/layouts/ArticleBottomLayout.astro";
import ArticleContent from "@/components/ArticleContent.astro";
// Create the static blog pages
export async function getStaticPaths() {
@ -12,7 +13,7 @@ export async function getStaticPaths() {
return posts
.filter((post) => !post.data.draft)
.map((post) => ({
params: { slug: post.slug },
params: { slug: post.id },
props: post,
}));
}
@ -26,13 +27,14 @@ const { title, summary } = post.data;
<PageLayout
title={title}
description={summary}
image={"/open-graph/blog/" + post.slug + ".png"}
image={"/open-graph/blog/" + post.id + ".png"}
>
<TopLayout>
<div class="animate">
<ArticleTopLayout entry={post} />
</div>
</TopLayout>
<ArticleContent entry={post} />
<BottomLayout>
<div class="animate">
<ArticleBottomLayout entry={post} />

View File

@ -1,23 +1,23 @@
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import Blog from "@components/Blog";
import { BLOG } from "@consts";
import PageLayout from "@/layouts/PageLayout.astro";
import TopLayout from "@/layouts/TopLayout.astro";
import BottomLayout from "@/layouts/BottomLayout.astro";
import { BLOG } from "@/consts";
import Blog from "@/components/Blog";
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const tags = [...new Set(posts.flatMap((post) => post.data.tags))].sort(
(a, b) => a.localeCompare(b)
(a, b) => a.localeCompare(b),
);
---
<PageLayout title={BLOG.TITLE} description={BLOG.DESCRIPTION}>
<TopLayout>
<div class="animate page-heading">
<div class="animate font-departure page-heading">
{BLOG.TITLE}
</div>
</TopLayout>

View File

@ -1,11 +1,9 @@
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import ArrowCard from "@components/ArrowCard";
import StackCard from "@components/StackCard.astro";
import { SITE, SOCIALS } from "@consts";
import TwinklingStars from "@components/TwinklingStars.astro";
import MeteorShower from "@components/MeteorShower.astro";
import PageLayout from "@/layouts/PageLayout.astro";
import ArrowCard from "@/components/ArrowCard";
import StackCard from "@/components/StackCard.astro";
import { SITE, SOCIALS } from "@/consts";
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
@ -20,65 +18,41 @@ const projects = (await getCollection("projects"))
const stack = [
{
text: "Astro",
icon: "astro",
icon: "material-icon-theme:astro",
href: "https://astro.build",
},
{
text: "Javascript",
icon: "javascript",
icon: "devicon:javascript",
href: "https://www.javascript.com",
},
{
text: "Typescript",
icon: "typescript",
icon: "devicon:typescript",
href: "https://www.typescriptlang.org",
},
{
text: "Tailwind",
icon: "tailwind",
icon: "devicon:tailwindcss",
href: "https://tailwindcss.com",
},
];
---
<PageLayout title="Home" description={SITE.DESCRIPTION}>
<!-- Light Mode: Particles -->
<div class="absolute inset-0 block dark:hidden">
<div id="particles1" class="fixed inset-0"></div>
<div id="particles2" class="fixed inset-0"></div>
<div id="particles3" class="fixed inset-0"></div>
</div>
<!-- Dark Theme: Stars -->
<div class="absolute inset-0 bg-black hidden dark:block">
<div id="stars1" class="fixed inset-0"></div>
<div id="stars2" class="fixed inset-0"></div>
<div id="stars3" class="fixed inset-0"></div>
</div>
<!-- Dark Theme: Twinkling Stars / Metors -->
<div id="galaxy" class="fixed inset-0">
<div class="hidden dark:block">
<TwinklingStars />
<MeteorShower />
</div>
</div>
<script is:inline src="/js/bg.js"></script>
<!-- HERO -->
<section class="relative h-screen w-full">
<section class="relative h-[75vh] w-full">
<div
id="planetcont"
class="animate absolute inset-0 top-1/4 overflow-hidden"
>
<div
id="crescent"
class="absolute top-0 left-1/2 -translate-x-1/2 w-[250vw] min-h-[100vh] aspect-square rounded-full p-[1px] bg-gradient-to-b from-black/25 dark:from-white/75 from-0% to-transparent to-5%"
class="absolute top-0 left-1/2 -translate-x-1/2 w-[250vw] min-h-screen aspect-square rounded-full p-px bg-linear-to-b from-black/25 dark:from-white/75 from-0% to-transparent to-5%"
>
<div
id="planet"
class="w-full h-full bg-white dark:bg-black rounded-full p-[1px] overflow-hidden flex justify-center"
class="w-full h-full bg-zinc-300 dark:bg-zinc-900 rounded-full p-px overflow-hidden flex justify-center"
>
<div
id="blur"
@ -101,7 +75,7 @@ const stack = [
Hello, I am Moritz
</p>
<p
class="animated text-2xl md:text-3xl lg:text-4xl font-bold uppercase text-black dark:text-white"
class="animated text-2xl md:text-3xl lg:text-4xl font-bold font-departure uppercase text-black dark:text-white"
>
Computer Science Student
</p>
@ -116,13 +90,13 @@ const stack = [
>
<a
href="/blog/"
class="py-2 px-4 rounded truncate text-xs md:text-sm lg:text-base bg-black dark:bg-white text-white dark:text-black hover:opacity-75 blend"
class="py-2 px-4 rounded truncate text-xs md:text-sm lg:text-base font-departure bg-zinc-900 dark:bg-zinc-300 text-white dark:text-black hover:opacity-75 blend"
>
Read my blog
</a>
<a
href="/work/"
class="py-2 px-4 truncate rounded text-xs md:text-sm lg:text-base border border-black/25 dark:border-white/25 hover:bg-black/5 hover:dark:bg-white/15 blend"
class="py-2 px-4 truncate rounded text-xs md:text-sm lg:text-base font-departure border border-black/25 dark:border-white/25 hover:bg-zinc-900/5 hover:dark:bg-white/15 blend"
>
View my work
</a>
@ -132,7 +106,7 @@ const stack = [
</div>
</section>
<div class="relative bg-white dark:bg-black">
<div class="relative bg-zinc-300 dark:bg-zinc-900">
<div class="mx-auto max-w-screen-sm p-5 space-y-24 pb-16">
<!-- About Section -->
<section class="animate">
@ -142,16 +116,22 @@ const stack = [
a cli-tool. I am motivated by challenging projects where
I have to research and learn new concepts.
</p>
<p>Now</p>
<p class="font-semibold font-departure">Now</p>
<p>
I currently study computer science at
I am currently pursuing a Master's degree in Computer
Science at
<a
href="https://upb.de"
target="_blank"
rel="noopener noreferrer">Paderborn University</a
>
with economics as my minor subject, while also getting a
peek at electrical engineering & maths in my regular subject.
rel="noopener noreferrer"
class="font-departure">Paderborn University</a
>. During my bachelor's program, I gained insight into
electrical engineering and mathematics. Through
extracurricular studies, I have learned about economics.
In the master's program, I can take a broader variety of
courses, including quantum computation, security,
machine learning, and lesser-known fields such as
knowledge representation.
</p>
</article>
</section>
@ -160,7 +140,9 @@ const stack = [
<section class="animate">
<div class="space-y-4">
<div class="flex justify-between">
<p class="font-semibold text-black dark:text-white">
<p
class="font-semibold font-departure text-black dark:text-white"
>
Recent posts
</p>
<a
@ -168,7 +150,7 @@ const stack = [
class="w-fit col-span-3 group flex gap-1 items-center underline decoration-[.5px] decoration-black/25 dark:decoration-white/50 hover:decoration-black dark:hover:decoration-white text-black dark:text-white underline-offset-2 blend"
>
<span
class="text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend"
class="font-departure text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend"
>
All posts
</span>
@ -186,31 +168,13 @@ const stack = [
</div>
</section>
<!-- Tech Stack Section -->
<section class="animate">
<div class="space-y-4">
<p class="font-semibold text-black dark:text-white">
Website build with
</p>
<div class="flex flex-wrap items-center gap-2 mt-5">
{
stack.map((item) => (
<StackCard
text={item.text}
icon={item.icon}
href={item.href}
/>
))
}
</div>
</div>
</section>
<!-- Project Preview Section -->
<section class="animate">
<div class="space-y-4">
<div class="flex justify-between">
<p class="font-semibold text-black dark:text-white">
<p
class="font-semibold font-departure text-black dark:text-white"
>
Recent projects
</p>
<a
@ -218,7 +182,7 @@ const stack = [
class="w-fit col-span-3 group flex gap-1 items-center underline decoration-[.5px] decoration-black/25 dark:decoration-white/50 hover:decoration-black dark:hover:decoration-white text-black dark:text-white underline-offset-2 blend"
>
<span
class="text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend"
class="font-departure text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend"
>
All projects
</span>
@ -239,10 +203,11 @@ const stack = [
<!-- Contact Section -->
<section class="animate">
<div>
<p class="font-semibold text-black dark:text-white">
<p
class="font-semibold font-departure text-black dark:text-white"
>
Let's Connect
</p>
<p>Reach out to me via email or on social media.</p>
<div class="grid grid-cols-4 gap-y-2 mt-4 auto-cols-min">
{
SOCIALS.map((social) => (
@ -269,6 +234,28 @@ const stack = [
</div>
</div>
</section>
<!-- Tech Stack Section -->
<section class="animate">
<div class="space-y-4">
<p
class="font-semibold font-departure text-black dark:text-white"
>
Website build with
</p>
<div class="flex flex-wrap items-center gap-2 mt-5">
{
stack.map((item) => (
<StackCard
text={item.text}
icon={item.icon}
href={item.href}
/>
))
}
</div>
</div>
</section>
</div>
</div>
</PageLayout>

View File

@ -6,26 +6,26 @@ const projects = Object.fromEntries(
(await getCollection("projects"))
.filter((project) => !project.data.draft)
.map((project) => [
"projects/" + project.slug,
"projects/" + project.id,
{
title: project.data.title,
description: project.data.summary,
},
])
]),
);
const blogs = Object.fromEntries(
(await getCollection("blog"))
.filter((blog) => !blog.data.draft)
.map((blog) => [
"blog/" + blog.slug,
"blog/" + blog.id,
{
title: blog.data.title,
description: blog.data.summary,
},
])
]),
);
export const { getStaticPaths, GET } = OGImageRoute({
export const { getStaticPaths, GET } = await OGImageRoute({
param: "route",
// A collection of pages to generate images for.

View File

@ -1,10 +1,11 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import ArticleTopLayout from "@layouts/ArticleTopLayout.astro";
import ArticleBottomLayout from "@layouts/ArticleBottomLayout.astro";
import PageLayout from "@/layouts/PageLayout.astro";
import TopLayout from "@/layouts/TopLayout.astro";
import BottomLayout from "@/layouts/BottomLayout.astro";
import ArticleTopLayout from "@/layouts/ArticleTopLayout.astro";
import ArticleBottomLayout from "@/layouts/ArticleBottomLayout.astro";
import ArticleContent from "@/components/ArticleContent.astro";
// Create the static projects pages
export async function getStaticPaths() {
@ -12,7 +13,7 @@ export async function getStaticPaths() {
return projects
.filter((project) => !project.data.draft)
.map((project) => ({
params: { slug: project.slug },
params: { slug: project.id },
props: project,
}));
}
@ -26,13 +27,14 @@ const { title, summary } = project.data;
<PageLayout
title={title}
description={summary}
image={"/open-graph/projects/" + project.slug + ".png"}
image={"/open-graph/projects/" + project.id + ".png"}
>
<TopLayout>
<div class="animate">
<ArticleTopLayout entry={project} />
</div>
</TopLayout>
<ArticleContent entry={project} />
<BottomLayout>
<div class="animate">
<ArticleBottomLayout entry={project} />

View File

@ -1,10 +1,10 @@
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import Projects from "@components/Projects";
import { PROJECTS } from "@consts";
import PageLayout from "@/layouts/PageLayout.astro";
import TopLayout from "@/layouts/TopLayout.astro";
import BottomLayout from "@/layouts/BottomLayout.astro";
import Projects from "@/components/Projects";
import { PROJECTS } from "@/consts";
const projects = (await getCollection("projects"))
.filter((project) => !project.data.draft)
@ -17,7 +17,7 @@ const tags = [
<PageLayout title={PROJECTS.TITLE} description={PROJECTS.DESCRIPTION}>
<TopLayout>
<div class="animate page-heading">
<div class="animate font-departure page-heading">
{PROJECTS.TITLE}
</div>
</TopLayout>

View File

@ -1,6 +1,6 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { SITE } from "@consts";
import { SITE } from "@/consts";
type Context = {
site: string;
@ -17,24 +17,27 @@ export async function GET(context: Context) {
}));
const items = [...posts, ...projects].filter(
({ item }) => !item.data.draft
({ item }) => !item.data.draft,
);
items.sort(
(a, b) =>
new Date(b.item.data.date).getTime() -
new Date(a.item.data.date).getTime()
new Date(a.item.data.date).getTime(),
);
return rss({
title: SITE.TITLE,
title: `${SITE.TITLE} - ${SITE.AUTHOR}`,
description: SITE.DESCRIPTION,
site: context.site,
stylesheet: "/rss-style.xsl",
items: items.map(({ item, pre }) => ({
title: item.data.title,
description: item.data.summary,
author: SITE.AUTHOR,
pubDate: item.data.date,
link: `/${pre}/${item.slug}/`,
link: `/${item.collection}/${item.id}/`,
customData: `<entryType>${item.collection}</entryType>`,
})),
});
}

View File

@ -1,15 +1,15 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import Search from "@components/Search";
import { SEARCH } from "@consts";
import PageLayout from "@/layouts/PageLayout.astro";
import TopLayout from "@/layouts/TopLayout.astro";
import BottomLayout from "@/layouts/BottomLayout.astro";
import Search from "@/components/Search";
import { SEARCH } from "@/consts";
const posts = (await getCollection("blog")).filter((post) => !post.data.draft);
const projects = (await getCollection("projects")).filter(
(post) => !post.data.draft
(post) => !post.data.draft,
);
const data = [...posts, ...projects] as CollectionEntry<"blog">[];
@ -17,7 +17,7 @@ const data = [...posts, ...projects] as CollectionEntry<"blog">[];
<PageLayout title={SEARCH.TITLE} description={SEARCH.DESCRIPTION}>
<TopLayout>
<div class="animate page-heading">
<div class="animate page-heading font-departure">
{SEARCH.TITLE}
</div>
</TopLayout>

View File

@ -1,23 +1,23 @@
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import { WORK } from "@consts";
import { getCollection, render } from "astro:content";
import PageLayout from "@/layouts/PageLayout.astro";
import TopLayout from "@/layouts/TopLayout.astro";
import BottomLayout from "@/layouts/BottomLayout.astro";
import { WORK } from "@/consts";
const collection = await getCollection("work");
collection.sort(
(a, b) =>
new Date(b.data.dateStart).getTime() -
new Date(a.data.dateStart).getTime()
new Date(a.data.dateStart).getTime(),
);
const work = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
const { Content } = await render(item);
return { ...item, Content };
})
}),
);
function formatWorkDate(input: Date | string) {
@ -43,14 +43,14 @@ function formatWorkDate(input: Date | string) {
{
work.map((entry) => (
<li class="animate border-b border-black/10 dark:border-white/25 mt-4 py-8 first-of-type:mt-0 first-of-type:pt-0 last-of-type:border-none">
<div class="text-sm uppercase mb-4">
<div class="font-departure text-sm uppercase mb-4">
{formatWorkDate(entry.data.dateStart)} -{" "}
{formatWorkDate(entry.data.dateEnd)}
</div>
<div class="text-black dark:text-white font-semibold">
<div class="font-departure text-black dark:text-white font-semibold">
{entry.data.company}
</div>
<div class="text-sm font-semibold">
<div class="font-departure text-sm font-semibold">
{entry.data.role}
</div>
<article class="prose dark:prose-invert">

View File

@ -1,148 +1,101 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@config "../../tailwind.config.js";
@layer base {
@font-face {
font-family: "Atkinson";
src: url("/fonts/atkinson-regular.woff") format("woff");
font-family: "DepartureMono";
src: url("/fonts/DepartureMono-Regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Atkinson";
src: url("/fonts/atkinson-bold.woff") format("woff");
font-family: "FiraCode";
src: url("/fonts/FiraCode-Regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "FiraCode";
src: url("/fonts/FiraCode-Bold.woff") format("woff");
font-weight: 700;
font-style: normal;
font-display: swap;
}
}
html {
overflow-y: scroll;
color-scheme: light;
background-color: white;
font-family: "Atkinson", sans-serif;
}
html.dark {
color-scheme: dark;
background-color: black;
}
html,
body {
@apply h-full w-full antialiased;
@apply bg-white dark:bg-black;
@apply text-black/75 dark:text-white/75;
}
body {
@apply relative flex flex-col;
}
main {
@apply flex flex-col flex-1 bg-white dark:bg-black;
}
header {
@apply border-b;
@apply transition-all duration-300 ease-in-out;
}
header:not(.scrolled) {
@apply bg-transparent border-transparent;
}
header.scrolled {
@apply bg-white/75 dark:bg-black/50;
@apply border-black/10 dark:border-white/25;
@apply backdrop-blur-sm saturate-200;
}
article {
@apply prose dark:prose-invert max-w-full pb-12;
}
.page-heading {
@apply font-semibold text-black dark:text-white;
}
.blend {
@apply transition-all duration-300 ease-in-out;
}
/** Light theme particles on home page */
@keyframes animateParticle {
from {
transform: translateY(0px);
html {
overflow-y: scroll;
color-scheme: light;
background-color: oklch(87.1% 0.006 286.286);
font-family: "FiraCode", monospace;
}
to {
transform: translateY(-2000px);
html.dark {
color-scheme: dark;
background-color: oklch(21% 0.006 285.885);
}
html,
body {
@apply h-full w-full antialiased;
@apply bg-white dark:bg-zinc-900;
@apply text-black/75 dark:text-white/75;
}
body {
@apply relative flex flex-col;
}
main {
@apply flex flex-col flex-1 bg-zinc-300 dark:bg-zinc-900;
}
header {
@apply border-b;
@apply transition-all duration-300 ease-in-out;
}
header:not(.scrolled) {
@apply bg-transparent border-transparent;
}
header.scrolled {
@apply bg-zinc-200/75 dark:bg-zinc-950/50;
@apply border-black/10 dark:border-white/25;
@apply backdrop-blur-sm saturate-200;
}
article {
@apply prose dark:prose-invert max-w-full pb-12;
}
.page-heading {
@apply font-semibold text-black dark:text-white;
}
.blend {
@apply transition-all duration-300 ease-in-out;
}
.animate {
opacity: 0;
transform: translateY(5px);
transition: opacity 1s ease, transform 1s ease;
}
.animate.show {
opacity: 1;
transform: translateY(0);
}
article img {
padding-top: 20px;
padding-bottom: 20px;
display: block;
margin: 0 auto;
}
}
/** styles for public /animation.js */
.animate {
opacity: 0;
transform: translateY(50px);
transition: opacity 1s ease, transform 1s ease;
}
.animate.show {
opacity: 1;
transform: translateY(0);
}
article img {
padding-top: 20px;
padding-bottom: 20px;
display: block;
margin: 0 auto;
}
/**
* TWINKLE STARS
*/
#twinkle-star.template {
@apply absolute -left-full; /* hide offscreen */
}
#twinkle-star.twinkle {
@apply animate-twinkle; /* defined in tailwind.config */
}
/**
* Meteors
*/
#meteors .shower {
@apply absolute inset-0 top-0;
@apply left-1/2 -translate-x-1/2;
@apply w-screen aspect-square;
}
#meteors .meteor {
@apply animate-meteor; /* defined in tailwind.config */
@apply absolute top-1/2 left-1/2 w-px h-[75vh];
@apply bg-gradient-to-b from-white to-transparent;
}
#meteors .shower.ur {
@apply rotate-45;
}
#meteors .shower.dr {
@apply rotate-135;
}
#meteors .shower.dl {
@apply rotate-225;
}
#meteors .shower.ul {
@apply rotate-315;
}

33
tailwind.config.js Normal file
View File

@ -0,0 +1,33 @@
import defaultTheme from "tailwindcss/defaultTheme";
import typography from "@tailwindcss/typography";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
safelist: [
"font-departure"
],
theme: {
extend: {
fontFamily: {
mono: ['"FiraCode"', ...defaultTheme.fontFamily.mono],
departure: ['"DepartureMono"', ...defaultTheme.fontFamily.mono],
},
typography: {
DEFAULT: {
css: {
maxWidth: "full",
},
},
},
rotate: {
45: "45deg",
135: "135deg",
225: "225deg",
315: "315deg",
},
},
},
plugins: [typography],
};

View File

@ -1,62 +0,0 @@
import defaultTheme from "tailwindcss/defaultTheme";
import typography from "@tailwindcss/typography";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
fontFamily: {
sans: ["Atkinson", ...defaultTheme.fontFamily.sans],
},
typography: {
DEFAULT: {
css: {
maxWidth: "full",
},
},
},
rotate: {
45: "45deg",
135: "135deg",
225: "225deg",
315: "315deg",
},
animation: {
twinkle: "twinkle 2s ease-in-out forwards",
meteor: "meteor 3s ease-in-out forwards",
},
keyframes: {
twinkle: {
"0%": {
opacity: 0,
transform: "rotate(0deg)",
},
"50%": {
opacity: 1,
transform: "rotate(180deg)",
},
"100%": {
opacity: 0,
transform: "rotate(360deg)",
},
},
meteor: {
"0%": {
opacity: 0,
transform: "translateY(200%)",
},
"50%": {
opacity: 1,
},
"100%": {
opacity: 0,
transform: "translateY(0)",
},
},
},
},
},
plugins: [typography],
};

View File

@ -4,7 +4,7 @@
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@*": ["src/*"]
"@/*": ["src/*"]
},
"jsx": "preserve",
"jsxImportSource": "solid-js"