Added 404, Linktree, moved blog to /blog from index, improved markdown frontmatter to accept tags
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +1,12 @@
|
|||||||
---
|
---
|
||||||
|
import type { Props as AstroProps } from 'astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
const { tags = [] } = Astro.props;
|
||||||
|
const selectedTag = Astro.url.searchParams.get('tag');
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ label: "github", href: "https://github.com/DerGrumpf", icon: "/github.png" },
|
{ label: "github", href: "https://github.com/DerGrumpf", icon: "/github.png" },
|
||||||
{ label: "gitea", href: "https://git.cyperpunk.de", icon: "/git.png" },
|
{ label: "gitea", href: "https://git.cyperpunk.de", icon: "/git.png" },
|
||||||
@@ -30,13 +38,15 @@ const links = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sidebar-topics" class="flex flex-col gap-2">
|
<div id="sidebar-topics" class="flex flex-col gap-2">
|
||||||
<span class="section-label text-ctp-overlay text-xs uppercase tracking-widest">topics</span>
|
<span class="section-label text-ctp-overlay text-xs uppercase tracking-widest">topics</span>
|
||||||
<div id="tags" class="flex flex-wrap gap-1">
|
<div id="tags" class="flex flex-wrap gap-1">
|
||||||
<span class="tag text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent hover:bg-ctp-overlay cursor-pointer transition-colors">Blog</span>
|
{tags.map((tag) => (
|
||||||
<span class="tag text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent hover:bg-ctp-overlay cursor-pointer transition-colors">Git Projects</span>
|
<a href={`/?tag=${tag}`} class={`tag text-xs px-2 py-0.5 rounded-full cursor-pointer transition-colors ${selectedTag === tag ? 'bg-ctp-accent text-ctp-base' : 'bg-ctp-surface text-ctp-subtext hover:text-ctp-accent'}`}>
|
||||||
<span class="tag text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent hover:bg-ctp-overlay cursor-pointer transition-colors">Matrix</span>
|
{tag}
|
||||||
</div>
|
</a>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="theme-switcher" class="flex gap-2 mt-auto pt-4 border-t border-ctp-surface justify-center">
|
<div id="theme-switcher" class="flex gap-2 mt-auto pt-4 border-t border-ctp-surface justify-center">
|
||||||
<button data-flavor="mocha" class="theme-dot w-4 h-4 rounded-full bg-[#cba6f7] hover:scale-125 transition-transform" title="Mocha"></button>
|
<button data-flavor="mocha" class="theme-dot w-4 h-4 rounded-full bg-[#cba6f7] hover:scale-125 transition-transform" title="Mocha"></button>
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
<canvas id="snake-canvas" class="rounded-lg" style="
|
||||||
|
padding: 2px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-image: linear-gradient(var(--ctp-base), var(--ctp-base)), linear-gradient(to right, var(--ctp-red), var(--ctp-peach), var(--ctp-yellow), var(--ctp-green), var(--ctp-teal), var(--ctp-blue), var(--ctp-mauve));
|
||||||
|
background-origin: border-box;
|
||||||
|
background-clip: padding-box, border-box;
|
||||||
|
"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('snake-canvas') as HTMLCanvasElement;
|
||||||
|
const size = 15;
|
||||||
|
const cols = 27;
|
||||||
|
const gameRows = 26;
|
||||||
|
const uiHeight = 30;
|
||||||
|
canvas.width = cols * size;
|
||||||
|
canvas.height = gameRows * size + uiHeight;
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
const rows = gameRows;
|
||||||
|
|
||||||
|
let snake: {x: number, y: number}[] = [];
|
||||||
|
let dir = { x: 1, y: 0 };
|
||||||
|
let food = { x: 0, y: 0 };
|
||||||
|
let running = false;
|
||||||
|
let aiMode = false;
|
||||||
|
let aiPath: {x: number, y: number}[] = [];
|
||||||
|
let interval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
function getColor(v: string) {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue(v).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomFood() {
|
||||||
|
let f;
|
||||||
|
do { f = { x: Math.floor(Math.random() * cols), y: Math.floor(Math.random() * rows) }; }
|
||||||
|
while (snake.some(s => s.x === f.x && s.y === f.y));
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function astar(start: {x:number,y:number}, goal: {x:number,y:number}, blocked: Set<string>) {
|
||||||
|
const key = (p: {x:number,y:number}) => `${p.x},${p.y}`;
|
||||||
|
const h = (p: {x:number,y:number}) => Math.abs(p.x - goal.x) + Math.abs(p.y - goal.y);
|
||||||
|
const open = [{ pos: start, g: 0, f: h(start), path: [] as {x:number,y:number}[] }];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
while (open.length) {
|
||||||
|
open.sort((a, b) => a.f - b.f);
|
||||||
|
const current = open.shift()!;
|
||||||
|
const k = key(current.pos);
|
||||||
|
if (visited.has(k)) continue;
|
||||||
|
visited.add(k);
|
||||||
|
if (current.pos.x === goal.x && current.pos.y === goal.y) return current.path;
|
||||||
|
for (const d of [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]) {
|
||||||
|
const next = { x: current.pos.x + d.x, y: current.pos.y + d.y };
|
||||||
|
if (next.x < 0 || next.x >= cols || next.y < 0 || next.y >= rows) continue;
|
||||||
|
if (blocked.has(key(next))) continue;
|
||||||
|
if (visited.has(key(next))) continue;
|
||||||
|
const g = current.g + 1;
|
||||||
|
open.push({ pos: next, g, f: g + h(next), path: [...current.path, next] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aiStep() {
|
||||||
|
const blocked = new Set(snake.slice(0, -1).map(s => `${s.x},${s.y}`));
|
||||||
|
const path = astar(snake[0], food, blocked);
|
||||||
|
if (path && path.length > 0) {
|
||||||
|
aiPath = path;
|
||||||
|
const next = path[0];
|
||||||
|
dir = { x: next.x - snake[0].x, y: next.y - snake[0].y };
|
||||||
|
} else {
|
||||||
|
aiPath = [];
|
||||||
|
const options = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
|
||||||
|
for (const d of options) {
|
||||||
|
const next = { x: snake[0].x + d.x, y: snake[0].y + d.y };
|
||||||
|
if (next.x >= 0 && next.x < cols && next.y >= 0 && next.y < rows && !blocked.has(`${next.x},${next.y}`)) {
|
||||||
|
dir = d;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGrid() {
|
||||||
|
ctx.strokeStyle = getColor('--ctp-surface');
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
for (let x = 0; x <= canvas.width; x += size) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(x, uiHeight); ctx.lineTo(x, canvas.height); ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = uiHeight; y <= canvas.height; y += size) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawUI() {
|
||||||
|
const btnW = 80;
|
||||||
|
const btnH = 20;
|
||||||
|
const btnY = (uiHeight - btnH) / 2;
|
||||||
|
const totalW = btnW * 2 + 10;
|
||||||
|
const startX = (canvas.width - totalW) / 2;
|
||||||
|
|
||||||
|
// Manual button
|
||||||
|
ctx.fillStyle = !aiMode ? getColor('--ctp-accent') : getColor('--ctp-surface');
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(startX, btnY, btnW, btnH, 4);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = !aiMode ? getColor('--ctp-base') : getColor('--ctp-subtext');
|
||||||
|
ctx.font = '12px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText('Manual', startX + btnW / 2, uiHeight / 2);
|
||||||
|
|
||||||
|
// AI button
|
||||||
|
const aiX = startX + btnW + 10;
|
||||||
|
ctx.fillStyle = aiMode ? getColor('--ctp-accent') : getColor('--ctp-surface');
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(aiX, btnY, btnW, btnH, 4);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = aiMode ? getColor('--ctp-base') : getColor('--ctp-subtext');
|
||||||
|
ctx.fillText('AI', aiX + btnW / 2, uiHeight / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.fillStyle = getColor('--ctp-base');
|
||||||
|
ctx.fillRect(0, uiHeight, canvas.width, canvas.height - uiHeight);
|
||||||
|
drawGrid();
|
||||||
|
drawUI();
|
||||||
|
|
||||||
|
// draw AI path
|
||||||
|
if (aiMode && aiPath.length > 0) {
|
||||||
|
ctx.fillStyle = `color-mix(in srgb, ${getColor('--ctp-teal')} 25%, transparent)`;
|
||||||
|
aiPath.forEach(p => {
|
||||||
|
ctx.fillRect(p.x * size, uiHeight + p.y * size, size - 1, size - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = getColor('--ctp-accent');
|
||||||
|
snake.forEach(s => ctx.fillRect(s.x * size, uiHeight + s.y * size, size - 1, size - 1));
|
||||||
|
|
||||||
|
ctx.fillStyle = getColor('--ctp-red');
|
||||||
|
ctx.fillRect(food.x * size, uiHeight + food.y * size, size - 1, size - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWaiting() {
|
||||||
|
ctx.fillStyle = getColor('--ctp-base');
|
||||||
|
ctx.fillRect(0, uiHeight, canvas.width, canvas.height - uiHeight);
|
||||||
|
drawGrid();
|
||||||
|
drawUI();
|
||||||
|
ctx.fillStyle = getColor('--ctp-subtext');
|
||||||
|
ctx.font = '14px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText('Press SPACE to start', canvas.width / 2, uiHeight + (canvas.height - uiHeight) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGameOver() {
|
||||||
|
ctx.fillStyle = getColor('--ctp-base');
|
||||||
|
ctx.fillRect(0, uiHeight, canvas.width, canvas.height - uiHeight);
|
||||||
|
drawGrid();
|
||||||
|
drawUI();
|
||||||
|
ctx.fillStyle = getColor('--ctp-text');
|
||||||
|
ctx.font = '20px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText('Game Over', canvas.width / 2, uiHeight + (canvas.height - uiHeight) / 2 - 16);
|
||||||
|
if (!aiMode) {
|
||||||
|
ctx.font = '14px monospace';
|
||||||
|
ctx.fillStyle = getColor('--ctp-subtext');
|
||||||
|
ctx.fillText('Press SPACE to restart', canvas.width / 2, uiHeight + (canvas.height - uiHeight) / 2 + 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
snake = [{ x: Math.floor(cols / 2), y: Math.floor(rows / 2) }];
|
||||||
|
dir = { x: 1, y: 0 };
|
||||||
|
food = randomFood();
|
||||||
|
aiPath = [];
|
||||||
|
running = true;
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
interval = setInterval(tick, aiMode ? 50 : 120);
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
if (aiMode) aiStep();
|
||||||
|
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
|
||||||
|
if (head.x < 0 || head.x >= cols || head.y < 0 || head.y >= rows || snake.some(s => s.x === head.x && s.y === head.y)) {
|
||||||
|
running = false;
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
drawGameOver();
|
||||||
|
if (aiMode) setTimeout(init, 800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snake.unshift(head);
|
||||||
|
if (head.x === food.x && head.y === food.y) {
|
||||||
|
food = randomFood();
|
||||||
|
} else {
|
||||||
|
snake.pop();
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(ai: boolean) {
|
||||||
|
aiMode = ai;
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
running = false;
|
||||||
|
aiPath = [];
|
||||||
|
if (ai) {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
drawWaiting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('click', (e) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const totalW = 80 * 2 + 10;
|
||||||
|
const startX = (canvas.width - totalW) / 2;
|
||||||
|
if (y < uiHeight) {
|
||||||
|
if (x >= startX && x <= startX + 80) setMode(false);
|
||||||
|
if (x >= startX + 90 && x <= startX + 170) setMode(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!aiMode && !running) init();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (aiMode) return;
|
||||||
|
if (e.key === 'ArrowUp' && dir.y === 0) dir = { x: 0, y: -1 };
|
||||||
|
if (e.key === 'ArrowDown' && dir.y === 0) dir = { x: 0, y: 1 };
|
||||||
|
if (e.key === 'ArrowLeft' && dir.x === 0) dir = { x: -1, y: 0 };
|
||||||
|
if (e.key === 'ArrowRight' && dir.x === 0) dir = { x: 1, y: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(() => drawWaiting());
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "Home", href: "/" },
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: "Blog", href: "/blog" },
|
||||||
{ label: "About", href: "/about" },
|
{ label: "About", href: "/about" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ const currentPath = Astro.url.pathname;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div id="topbar-inner" class="flex items-center justify-between px-6 h-12 bg-ctp-mantle border-b border-ctp-surface">
|
<div id="topbar-inner" class="flex items-center justify-between px-6 h-12 bg-ctp-mantle border-b border-ctp-surface">
|
||||||
<span id="logo" class="text-ctp-accent font-semibold text-sm tracking-wide">Cyperpunk.de</span>
|
<a href="/" id="logo" class="text-ctp-accent font-semibold text-sm tracking-wide">Cyperpunk.de</a>
|
||||||
<nav class="flex gap-6">
|
<nav class="flex gap-6">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const posts = defineCollection({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
date: z.date(),
|
date: z.date(),
|
||||||
published: z.boolean().default(false),
|
published: z.boolean().default(false),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: Markdown Test
|
title: Markdown Test
|
||||||
date: 2026-06-07
|
date: 2026-06-07
|
||||||
published: true
|
published: true
|
||||||
|
tags: [blog, nix, astro]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Heading 1
|
# Heading 1
|
||||||
@@ -27,7 +28,9 @@ This is a normal paragraph with **bold text**, *italic text*, and `inline code`.
|
|||||||
## Links and Images
|
## Links and Images
|
||||||
|
|
||||||
[Visit Gitea](https://git.cyperpunk.de)
|
[Visit Gitea](https://git.cyperpunk.de)
|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
<html lang="en" data-theme="mocha">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{title}</title>
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex items-center justify-center bg-ctp-base">
|
||||||
|
<slot />
|
||||||
|
<script>
|
||||||
|
const saved = localStorage.getItem('ctp-theme') ?? 'mocha';
|
||||||
|
document.documentElement.setAttribute('data-theme', saved);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import Blank from '../layouts/Blank.astro';
|
||||||
|
import Snake from '../components/Snake.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Blank title="404 - Don't Panic">
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-screen text-center gap-6">
|
||||||
|
<div id="error-code" class="font-bold text-ctp-accent leading-none" style="font-size: 5rem;">
|
||||||
|
404
|
||||||
|
</div>
|
||||||
|
<div class="text-ctp-text text-xl font-semibold">Don't Panic.</div>
|
||||||
|
<p class="text-ctp-subtext mx-auto text-sm hyphens-auto" style="max-width: 40%; text-align: justify;">
|
||||||
|
The Guide says: remain calm. This page exists somewhere in the universe — it simply cannot be located from your current position in space and time. While you wait, the Guide recommends a game of Snake. Or switch to AI mode and watch the snake find its own way — much like the universe itself, it knows where it is going, even if you do not.
|
||||||
|
</p>
|
||||||
|
<Snake />
|
||||||
|
<a href="/" class="text-ctp-accent hover:text-ctp-peach transition-colors text-sm">← Take me to the Guide</a>
|
||||||
|
</div>
|
||||||
|
</Blank>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#error-code {
|
||||||
|
animation: glitch 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch {
|
||||||
|
0% { text-shadow: 4px 0 var(--ctp-red), -4px 0 var(--ctp-blue); }
|
||||||
|
20% { text-shadow: -4px 0 var(--ctp-red), 4px 0 var(--ctp-blue); }
|
||||||
|
40% { text-shadow: 4px 4px var(--ctp-mauve), -4px -4px var(--ctp-green); }
|
||||||
|
60% { text-shadow: -4px 0 var(--ctp-peach), 4px 0 var(--ctp-teal); }
|
||||||
|
80% { text-shadow: 4px 0 var(--ctp-red), -4px 0 var(--ctp-blue); }
|
||||||
|
100% { text-shadow: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,16 +11,27 @@ export async function getStaticPaths() {
|
|||||||
|
|
||||||
const { post } = Astro.props;
|
const { post } = Astro.props;
|
||||||
const { Content, headings } = await render(post);
|
const { Content, headings } = await render(post);
|
||||||
|
|
||||||
|
const allPosts = await getCollection('posts', ({ data }) => data.published === true);
|
||||||
|
const allTags = [...new Set(allPosts.flatMap(p => p.data.tags))].sort();
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base title={post.data.title}>
|
<Base title={post.data.title}>
|
||||||
<Topbar slot="topbar" />
|
<Topbar slot="topbar" />
|
||||||
<Sidebar slot="sidebar" />
|
<Sidebar slot="sidebar" tags={allTags}/>
|
||||||
|
|
||||||
<article class="prose max-w-none">
|
<article class="prose max-w-none">
|
||||||
<Content />
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-1 mt-6">
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<a href={`/?tag=${tag}`} class="text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent transition-colors">
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toc-container" class="fixed top-16 right-4 z-50">
|
<div id="toc-container" class="fixed top-16 right-4 z-50">
|
||||||
<button id="toc-open" class="bg-ctp-surface text-ctp-accent p-2 rounded-lg hover:bg-ctp-overlay transition-colors" style="font-family: 'NerdFontsSymbols Nerd Font'" title="Table of contents">
|
<button id="toc-open" class="bg-ctp-surface text-ctp-accent p-2 rounded-lg hover:bg-ctp-overlay transition-colors" style="font-family: 'NerdFontsSymbols Nerd Font'" title="Table of contents">
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
export const prerender = false;
|
||||||
|
import Base from "../../layouts/Base.astro";
|
||||||
|
import Topbar from "../../components/Topbar.astro";
|
||||||
|
import Sidebar from "../../components/Sidebar.astro";
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
const allPosts = await getCollection('posts', ({ data }) => data.published === true);
|
||||||
|
allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
|
const selectedTag = Astro.url.searchParams.get('tag');
|
||||||
|
const posts = selectedTag
|
||||||
|
? allPosts.filter(p => p.data.tags.includes(selectedTag))
|
||||||
|
: allPosts;
|
||||||
|
const allTags = [...new Set(allPosts.flatMap(p => p.data.tags))].sort();
|
||||||
|
const grouped = posts.reduce((acc, post) => {
|
||||||
|
const date = post.data.date.toDateString();
|
||||||
|
if (!acc[date]) acc[date] = [];
|
||||||
|
acc[date].push(post);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, typeof posts>);
|
||||||
|
const groupedEntries = Object.entries(grouped);
|
||||||
|
---
|
||||||
|
<Base title="blog">
|
||||||
|
<Topbar slot="topbar" />
|
||||||
|
<Sidebar slot="sidebar" tags={allTags} />
|
||||||
|
{selectedTag && (
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-ctp-accent text-xl font-bold mb-1"># {selectedTag.charAt(0).toUpperCase() + selectedTag.slice(1)}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{groupedEntries.map(([date, datePosts]) => (
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="text-ctp-subtext text-xs uppercase tracking-widest mb-2 border-b border-ctp-surface pb-1">{date}</div>
|
||||||
|
{datePosts.map((post) => (
|
||||||
|
<div class="p-4 mb-3 rounded-lg bg-ctp-surface">
|
||||||
|
<a href={`/blog/${post.id}`} class="text-ctp-accent">{post.data.title}</a>
|
||||||
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
|
{post.data.tags.map((tag) => (
|
||||||
|
<a href={`/blog?tag=${tag}`} class="text-xs px-2 py-0.5 rounded-full bg-ctp-mantle text-ctp-subtext hover:text-ctp-accent transition-colors">
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Base>
|
||||||
@@ -1,21 +1,55 @@
|
|||||||
---
|
---
|
||||||
import Base from "../layouts/Base.astro";
|
import Blank from '../layouts/Blank.astro';
|
||||||
import Topbar from "../components/Topbar.astro";
|
|
||||||
import Sidebar from "../components/Sidebar.astro";
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
|
|
||||||
const posts = await getCollection('posts', ({ data }) => data.published === true);
|
const links = [
|
||||||
posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
{ label: "Blog", href: "/blog", icon: "/blog.png"},
|
||||||
|
{ label: "Github", href: "https://github.com/DerGrumpf", icon: "/github.png" },
|
||||||
|
{ label: "Gitea", href: "https://git.cyperpunk.de", icon: "/git.png" },
|
||||||
|
{ label: "Instagram", href: "https://www.instagram.com/dergrumpf/", icon: "/instagram.png" },
|
||||||
|
{ label: "Matrix", href: "https://matrix.to/#/@dergrumpf:cyperpunk.de", icon: "/matrix.png" },
|
||||||
|
{ label: "Spotify", href: "https://open.spotify.com/user/fackthatshiet?si=d502739d787e497e", icon: "/spotify.png" },
|
||||||
|
{ label: "RSS", href: "/rss.xml", icon: "/rss.png" },
|
||||||
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base title="home">
|
<Blank title="cyperpunk.de">
|
||||||
<Topbar slot="topbar" />
|
<div class="flex flex-col items-center gap-8">
|
||||||
<Sidebar slot="sidebar" />
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<img src="/avatar.jpg" alt="DerGrumpf" class="w-42 h-42 rounded-full object-cover" />
|
||||||
{posts.map((post) => (
|
<h1 class="text-ctp-text font-bold text-xl">Phil Keier</h1>
|
||||||
<div class="p-4 mb-3 rounded-lg bg-ctp-surface">
|
<p class="text-ctp-subtext text-sm">DerGrumpf</p>
|
||||||
<a href={`/posts/${post.id}`} class="text-ctp-accent">{post.data.title}</a>
|
<p class="text-ctp-subtext text-xs">Media Scientist · Programmer · Linux Enthusiast</p>
|
||||||
<p class="text-ctp-subtext text-xs">{post.data.date.toDateString()}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</Base>
|
<div class="flex flex-col gap-3 w-full" style="max-width: 360px;">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
class="flex items-center gap-4 px-5 py-3 rounded-lg bg-ctp-surface hover:bg-ctp-overlay text-ctp-text transition-colors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-5 h-5 shrink-0"
|
||||||
|
style={`mask-image: url(${link.icon}); mask-size: contain; mask-repeat: no-repeat; mask-position: center; background-color: var(--ctp-text);`}
|
||||||
|
></div>
|
||||||
|
<span class="text-sm font-semibold">{link.label}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 pt-4 border-t border-ctp-surface" style="width: 360px; justify-content: center;">
|
||||||
|
<button data-flavor="mocha" class="theme-dot w-3 h-3 rounded-full bg-[#cba6f7] hover:scale-125 transition-transform" title="Mocha"></button>
|
||||||
|
<button data-flavor="macchiato" class="theme-dot w-3 h-3 rounded-full bg-[#c6a0f6] hover:scale-125 transition-transform" title="Macchiato"></button>
|
||||||
|
<button data-flavor="frappe" class="theme-dot w-3 h-3 rounded-full bg-[#ca9ee6] hover:scale-125 transition-transform" title="Frappé"></button>
|
||||||
|
<button data-flavor="latte" class="theme-dot w-3 h-3 rounded-full bg-[#8839ef] hover:scale-125 transition-transform" title="Latte"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Blank>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.theme-dot').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const flavor = btn.getAttribute('data-flavor');
|
||||||
|
document.documentElement.setAttribute('data-theme', flavor);
|
||||||
|
localStorage.setItem('ctp-theme', flavor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -201,14 +201,14 @@
|
|||||||
--ctp-accent: var(--ctp-mauve);
|
--ctp-accent: var(--ctp-mauve);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="latte"] .icon-mono {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-mono {
|
.icon-mono {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="latte"] .icon-mono {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--ctp-base);
|
background-color: var(--ctp-base);
|
||||||
color: var(--ctp-text);
|
color: var(--ctp-text);
|
||||||
@@ -284,6 +284,18 @@ article a:hover {
|
|||||||
color: var(--ctp-peach);
|
color: var(--ctp-peach);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Images */
|
||||||
|
article img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: block;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 2px;
|
||||||
|
background: linear-gradient(to right, var(--ctp-red), var(--ctp-peach), var(--ctp-yellow), var(--ctp-green), var(--ctp-teal), var(--ctp-blue), var(--ctp-mauve));
|
||||||
|
}
|
||||||
|
|
||||||
/* Horizontal Line */
|
/* Horizontal Line */
|
||||||
article hr {
|
article hr {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -304,8 +316,7 @@ article hr {
|
|||||||
/* Code Block*/
|
/* Code Block*/
|
||||||
article pre code {
|
article pre code {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0;
|
padding: 0}
|
||||||
}
|
|
||||||
|
|
||||||
article code {
|
article code {
|
||||||
background-color: var(--ctp-surface);
|
background-color: var(--ctp-surface);
|
||||||
|
|||||||
Reference in New Issue
Block a user