Initial commit
This commit is contained in:
205
app/globals.css
Normal file
205
app/globals.css
Normal 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
62
app/layout.tsx
Normal 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
26
app/page.tsx
Normal 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
68
app/posts/[slug]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user