aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorschererleander <leander@schererleander.de>2025-12-25 23:33:25 +0000
committerschererleander <leander@schererleander.de>2025-12-25 23:33:25 +0000
commitd82fb3b552d20a279efdd9408042183cfa02fb48 (patch)
tree4ffe818e591e54da71f7592506c873abf0d9d481 /app
parentd7edbf05ab0e90eedcb99e4462e3a61793b2eff9 (diff)
initial commit
Diffstat (limited to 'app')
-rw-r--r--app/blog/[slug]/page.tsx35
-rw-r--r--app/blog/page.tsx34
-rw-r--r--app/favicon.icobin0 -> 25931 bytes
-rw-r--r--app/globals.css176
-rw-r--r--app/layout.tsx37
-rw-r--r--app/page.tsx41
6 files changed, 323 insertions, 0 deletions
diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx
new file mode 100644
index 0000000..a066779
--- /dev/null
+++ b/app/blog/[slug]/page.tsx
@@ -0,0 +1,35 @@
+// app/blog/[slug]/page.tsx
+import { format, parseISO } from 'date-fns'
+import { allPosts } from 'contentlayer/generated'
+import { MDXContent } from '@/components/mdx-content'
+
+export const generateStaticParams = async () => allPosts.map((post) => ({ slug: post._raw.flattenedPath }))
+
+export const generateMetadata = async ({ params }: { params: Promise<{ slug: string }> }) => {
+ const { slug } = await params
+ const post = allPosts.find((post) => post._raw.flattenedPath === slug)
+ if (!post) throw new Error(`Post not found for slug: ${slug}`)
+ return { title: post.title }
+}
+
+const PostLayout = async ({ params }: { params: Promise<{ slug: string }> }) => {
+ const { slug } = await params
+ const post = allPosts.find((post) => post._raw.flattenedPath === slug)
+ if (!post) throw new Error(`Post not found for slug: ${slug}`)
+
+ return (
+ <article className="mx-auto max-w-xl py-8">
+ <div className="mb-8">
+ <h1 className="text-3xl font-bold">{post.title}</h1>
+ <time dateTime={post.date} className="text-xs text-muted-foreground/60">
+ {format(parseISO(post.date), 'LLLL d, yyyy')}
+ </time>
+ </div>
+ <div className="prose dark:prose-invert prose-neutral max-w-none [&>*]:mb-3 [&>*:last-child]:mb-0">
+ <MDXContent code={post.body.code} />
+ </div>
+ </article>
+ )
+}
+
+export default PostLayout
diff --git a/app/blog/page.tsx b/app/blog/page.tsx
new file mode 100644
index 0000000..abc3394
--- /dev/null
+++ b/app/blog/page.tsx
@@ -0,0 +1,34 @@
+
+import { format, parseISO } from 'date-fns'
+import { allPosts } from 'contentlayer/generated'
+import Link from 'next/link'
+
+export default function BlogPage() {
+ const posts = allPosts.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
+
+ return (
+ <div className="max-w-2xl mx-auto py-12">
+ <h1 className="text-3xl font-bold mb-8">Blog</h1>
+ <div className="space-y-8">
+ {posts.map((post) => (
+ <article key={post._id} className="group relative flex flex-col items-start">
+ <h2 className="text-xl font-semibold tracking-tight">
+ <Link href={post.url} className="hover:underline">
+ {post.title}
+ </Link>
+ </h2>
+ <time dateTime={post.date} className="text-sm text-muted-foreground mb-2">
+ {format(parseISO(post.date), 'LLLL d, yyyy')}
+ </time>
+ </article>
+ ))}
+ </div>
+ </div>
+ )
+}
+
+function compareDesc(a: Date, b: Date) {
+ if (a > b) return -1
+ if (a < b) return 1
+ return 0
+}
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
--- /dev/null
+++ b/app/favicon.ico
Binary files differ
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..886b125
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,176 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --radius-2xl: calc(var(--radius) + 8px);
+ --radius-3xl: calc(var(--radius) + 12px);
+ --radius-4xl: calc(var(--radius) + 16px);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer utilities {
+ .noise {
+ background-image: url(/noise.png);
+ background-repeat: repeat;
+ background-size: 182px;
+ opacity: var(--noise-opacity);
+ }
+}
+
+@layer base {
+ :root {
+ --noise-opacity: 0.025;
+ --site-width: 630px;
+ }
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+
+ /* Shiki Syntax Highlighting */
+ figure[data-rehype-pretty-code-figure] pre {
+ @apply bg-muted/50 p-4 rounded-lg border overflow-x-auto;
+ }
+
+ figure[data-rehype-pretty-code-figure] code {
+ @apply grid text-sm;
+ }
+
+ code[data-line-numbers] {
+ counter-reset: line;
+ }
+
+ code[data-line-numbers] > [data-line]::before {
+ counter-increment: line;
+ content: counter(line);
+ @apply mr-4 inline-block w-4 text-right text-gray-500;
+ }
+
+ /* Anchor links for headings */
+ .prose .anchor {
+ @apply absolute no-underline opacity-0 transition-opacity;
+ margin-left: -1em;
+ padding-right: 0.5em;
+ width: 80%;
+ max-width: 700px;
+ cursor: pointer;
+ }
+
+ .prose .anchor:hover,
+ .prose *:hover > .anchor {
+ @apply opacity-100;
+ }
+
+ .prose .anchor::after {
+ @apply content-['#'] text-muted-foreground;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..36ca80c
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,37 @@
+import { Noise } from "@/components/noise";
+import { ThemeProvider } from "@/components/theme-provider"
+import { Footer } from "@/components/footer"
+import { Header } from "@/components/header"
+import type { Metadata } from "next";
+import "./globals.css";
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
+
+export default function Layout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+ <html lang="en" suppressHydrationWarning>
+ <body>
+ <ThemeProvider
+ attribute="class"
+ defaultTheme="system"
+ enableSystem
+ disableTransitionOnChange
+ >
+ <Header />
+ <main className="mx-auto max-w-[var(--site-width)]">
+ {children}
+ </main>
+ <Footer />
+ <Noise />
+ </ThemeProvider>
+ </body>
+ </html>
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..057842d
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,41 @@
+
+import { ToolsGrid } from "@/components/tools-grid";
+import { ProjectsGrid } from "@/components/projects-grid";
+import { PostCard } from "@/components/post-card";
+import { compareDesc } from 'date-fns'
+import { allPosts } from 'contentlayer/generated'
+import MapWrapper from "@/components/map-wrapper";
+
+export default function Home() {
+ const posts = allPosts.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
+
+ return (
+ <>
+ <div className="w-full relative z-0">
+ <MapWrapper />
+ </div>
+
+ <section className="text-center space-y-2 -mt-8 relative z-10 pointer-events-none mb-12">
+ <div className="pointer-events-auto">
+ <h2 className="text-2xl font-bold mix-blend-luminosity text-foreground">Hi, I&apos;m Leander.</h2>
+ <p className="text-muted-foreground/60 max-w-lg mx-auto">
+ Passionate about hardware & software, pursuing computer science studies. Currently building 3D-printing projects and exploring homelabing.
+ </p>
+ </div>
+ </section>
+
+ <section className="mt-8 mx-auto w-full">
+ <ToolsGrid />
+ </section>
+ <section className="mt-8 mx-auto w-full">
+ <ProjectsGrid />
+ </section>
+
+ <section className="mt-8 mx-auto w-full">
+ {posts.map((post, idx) => (
+ <PostCard key={idx} {...post} />
+ ))}
+ </section>
+ </>
+ );
+}