Initial commit

This commit is contained in:
2025-05-06 23:09:47 +02:00
commit 89e98efb7d
79 changed files with 6948 additions and 0 deletions

205
app/globals.css Normal file
View File

@ -0,0 +1,205 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 222 30% 7%; /* Darkened background */
--foreground: 210 20% 98%;
--card: 222 47% 9%; /* Darkened card */
--card-foreground: 210 20% 98%;
--popover: 222 47% 9%;
--popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 222 30% 12%; /* Darkened secondary */
--secondary-foreground: 210 20% 98%;
--muted: 222 30% 12%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 222 30% 12%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 222 30% 12%;
--input: 222 30% 12%;
--ring: 263.4 70% 50.4%;
}
}
@layer base {
body {
@apply bg-background text-foreground;
}
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animate-gradient {
background-size: 200% 200%;
animation: gradient 8s ease infinite;
}
/* Animated background gradients */
.animated-bg {
position: relative;
overflow: hidden;
}
.animated-bg::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle at center,
rgba(124, 58, 237, 0.03) 0%,
rgba(124, 58, 237, 0.01) 20%,
rgba(236, 72, 153, 0.01) 40%,
rgba(236, 72, 153, 0) 60%,
transparent 100%
);
animation: rotate 60s linear infinite;
z-index: -1;
}
.animated-bg::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle at center,
rgba(236, 72, 153, 0.03) 0%,
rgba(236, 72, 153, 0.01) 20%,
rgba(124, 58, 237, 0.01) 40%,
rgba(124, 58, 237, 0) 60%,
transparent 100%
);
animation: rotate 40s linear infinite reverse;
z-index: -1;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Modified post-link to always show the gradient line on hover */
.post-link {
position: relative;
display: inline-block;
transition: all 0.3s ease;
}
.post-link::after {
content: "";
position: absolute;
width: 0;
height: 2px;
bottom: -2px;
left: 0;
background: linear-gradient(90deg, #7c3aed, #ec4899);
transition: width 0.3s ease;
}
.post-card:hover .post-link::after {
width: 100%;
}
.post-card {
position: relative;
transition: transform 0.3s ease;
}
.post-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: 0.5rem;
padding: 1px;
background: linear-gradient(90deg, rgba(124, 58, 237, 0.2), rgba(236, 72, 153, 0.2));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 0.3s ease;
}
.post-card:hover {
transform: translateY(-2px);
}
.post-card:hover::before {
opacity: 1;
}
.prose {
@apply max-w-none;
}
.prose a {
@apply text-purple-400 hover:text-pink-400 transition-colors duration-200;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4 {
@apply text-foreground;
}
.prose code {
@apply bg-secondary text-foreground px-1 py-0.5 rounded;
}
.prose pre {
@apply bg-card border border-border/40 rounded-md;
}
.prose blockquote {
@apply border-l-4 border-purple-400 bg-secondary/50 pl-4 py-1 italic;
}
.prose img {
@apply rounded-md;
}
/* Ensure proper scrolling */
html,
body {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
scroll-behavior: smooth;
}
/* Ensure content can extend beyond viewport */
#__next,
main {
min-height: 100%;
display: flex;
flex-direction: column;
}
/* Reduce spacing between posts for more compact layout */
.space-y-6 > * + * {
margin-top: 1.25rem;
}

62
app/layout.tsx Normal file
View File

@ -0,0 +1,62 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { Code, ExternalLink, Linkedin } from "lucide-react"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "Minimalist Blog",
description: "A minimalist blog built with Next.js and Markdown",
generator: 'v0.dev'
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.className} min-h-screen bg-background animated-bg overflow-y-auto`}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<div className="container mx-auto px-4 py-8 max-w-3xl relative z-10 min-h-screen">
<header className="mb-12">
<h1 className="text-4xl font-bold tracking-tight bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent animate-gradient">
Minimalist Blog
</h1>
<div className="mt-4 flex flex-wrap gap-3">
<a
href="https://git.bechsor.no"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-4 py-2 rounded-md bg-secondary hover:bg-secondary/80 transition-colors duration-200 text-sm font-medium group"
>
<Code className="h-4 w-4 text-purple-400 group-hover:text-pink-400 transition-colors duration-200" />
<span>public code</span>
<ExternalLink className="h-3 w-3 opacity-70" />
</a>
<a
href="https://www.linkedin.com/in/jensbs/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-4 py-2 rounded-md bg-secondary hover:bg-secondary/80 transition-colors duration-200 text-sm font-medium group"
>
<Linkedin className="h-4 w-4 text-purple-400 group-hover:text-pink-400 transition-colors duration-200" />
<span>linkedin</span>
<ExternalLink className="h-3 w-3 opacity-70" />
</a>
</div>
</header>
<main>{children}</main>
<footer className="mt-20 pt-8 border-t border-border/40 text-muted-foreground text-sm">
<p>© {new Date().getFullYear()} Minimalist Blog. All rights reserved.</p>
</footer>
</div>
</ThemeProvider>
</body>
</html>
)
}

26
app/page.tsx Normal file
View File

@ -0,0 +1,26 @@
import { getAllPosts } from "@/lib/api"
import { PostPreview } from "@/components/post-preview"
// Make sure this is a Server Component (default in App Router)
export default function Home() {
// Use synchronous data fetching to avoid suspense issues
const posts = getAllPosts()
// Sort posts by date (newest first)
const sortedPosts = posts.sort((a, b) => new Date(b.metadata.date).getTime() - new Date(a.metadata.date).getTime())
return (
<div className="space-y-8 w-full">
<section>
<h2 className="sr-only">Blog posts</h2>
<ul className="space-y-6 w-full">
{sortedPosts.map((post) => (
<li key={post.slug} className="transition-all duration-300 w-full">
<PostPreview post={post} />
</li>
))}
</ul>
</section>
</div>
)
}

68
app/posts/[slug]/page.tsx Normal file
View File

@ -0,0 +1,68 @@
import { getPostBySlug, getAllPosts } from "@/lib/api"
import { notFound } from "next/navigation"
import type { Metadata } from "next"
import Link from "next/link"
import { ArrowLeft } from "lucide-react"
import { Suspense } from "react"
import { MDXContent } from "@/components/mdx-content"
export function generateStaticParams() {
const posts = getAllPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
const post = getPostBySlug(params.slug)
if (!post) {
return {
title: "Post Not Found",
}
}
return {
title: post.metadata.title,
description: post.metadata.excerpt,
}
}
export default function Post({ params }: { params: { slug: string } }) {
const post = getPostBySlug(params.slug)
if (!post) {
notFound()
}
return (
<article className="prose prose-invert max-w-none">
<Link
href="/"
className="inline-flex items-center text-muted-foreground hover:text-primary mb-8 no-underline transition-colors"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to all posts
</Link>
<header className="mb-8">
<h1 className="text-4xl font-bold tracking-tight mb-2 bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent">
{post.metadata.title}
</h1>
<time dateTime={post.metadata.date} className="text-muted-foreground">
{new Date(post.metadata.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
</header>
<div className="post-content">
<Suspense fallback={<div>Loading content...</div>}>
<MDXContent content={post.content} />
</Suspense>
</div>
</article>
)
}