From d82fb3b552d20a279efdd9408042183cfa02fb48 Mon Sep 17 00:00:00 2001 From: schererleander Date: Thu, 25 Dec 2025 23:33:25 +0000 Subject: initial commit --- .gitignore | 17 +- README.md | 36 + app/blog/[slug]/page.tsx | 35 + app/blog/page.tsx | 34 + app/favicon.ico | Bin 0 -> 25931 bytes app/globals.css | 176 + app/layout.tsx | 37 + app/page.tsx | 41 + components.json | 22 + components/footer.tsx | 20 + components/header.tsx | 42 + components/map-wrapper.tsx | 10 + components/map.tsx | 70 + components/mdx-content.tsx | 12 + components/noise.tsx | 8 + components/panda.tsx | 9 + components/post-card.tsx | 20 + components/projects-grid.tsx | 51 + components/theme-provider.tsx | 11 + components/tools-grid.tsx | 56 + components/ui/button.tsx | 58 + components/ui/card.tsx | 92 + contentlayer.config.ts | 45 + eslint.config.mjs | 19 + flake.nix | 90 +- lib/utils.ts | 6 + next-env.d.ts | 6 + next.config.mjs | 11 + package-lock.json | 11301 ++++++++++++++++++++++++++++++++++++ package.json | 44 + pnpm-workspace.yaml | 5 + postcss.config.mjs | 7 + posts/hello-world.mdx | 10 + posts/homelab.mdx | 31 + public/images/homelab/nas.webp | Bin 0 -> 233440 bytes public/images/homelab/nasfan.webp | Bin 0 -> 101030 bytes public/images/homelab/pi.webp | Bin 0 -> 181260 bytes public/noise.png | Bin 0 -> 33360 bytes tsconfig.json | 37 + 39 files changed, 12427 insertions(+), 42 deletions(-) create mode 100644 README.md create mode 100644 app/blog/[slug]/page.tsx create mode 100644 app/blog/page.tsx create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/footer.tsx create mode 100644 components/header.tsx create mode 100644 components/map-wrapper.tsx create mode 100644 components/map.tsx create mode 100644 components/mdx-content.tsx create mode 100644 components/noise.tsx create mode 100644 components/panda.tsx create mode 100644 components/post-card.tsx create mode 100644 components/projects-grid.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/tools-grid.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 contentlayer.config.ts create mode 100644 eslint.config.mjs create mode 100644 lib/utils.ts create mode 100644 next-env.d.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pnpm-workspace.yaml create mode 100644 postcss.config.mjs create mode 100644 posts/hello-world.mdx create mode 100644 posts/homelab.mdx create mode 100644 public/images/homelab/nas.webp create mode 100644 public/images/homelab/nasfan.webp create mode 100644 public/images/homelab/pi.webp create mode 100644 public/noise.png create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 54f07af..fc3e5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,19 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? + +# Next.js +.next/ +out/ +build/ + +# Contentlayer +.contentlayer + +# Environment variables +.env +.env*.local + +# Vercel +.vercel diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 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 ( +
+
+

{post.title}

+ +
+
+ +
+
+ ) +} + +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 ( +
+

Blog

+
+ {posts.map((post) => ( +
+

+ + {post.title} + +

+ +
+ ))} +
+
+ ) +} + +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 Binary files /dev/null and b/app/favicon.ico 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 ( + + + +
+
+ {children} +
+