Skip to content

Feature/Blogs #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions app/blogs/[year]/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from "rehype-highlight";
import "highlight.js/styles/github-dark.css";
import { Metadata } from "next";
import { ClockIcon } from "lucide-react";

const BLOG_DIR = path.join(process.cwd(), "content/blog");

async function getBlogContent(year: string, slug: string) {
const filePath = path.join(BLOG_DIR, year, `${slug}.md`);
const fileContent = fs.readFileSync(filePath, "utf8");
const { content, data } = matter(fileContent);
return { content, meta: data };
}

export async function generateMetadata({
params,
}: any): Promise<Metadata> {
const { year, slug } = await params;
const { meta } = await getBlogContent(year, slug);
return {
title: meta.title,
description: meta.description || `Read ${meta.title} on our blog`,
};
}

export default async function BlogPage({ params }: any) {
const { year, slug } = await params;
const { content, meta } = await getBlogContent(year, slug);

const minuteRead = content.length / 200;

return (
<div className="min-h-screen md:py-10 p-2">
<article className="max-w-3xl dark:bg-neutral-900 bg-neutral-100 shadow-sm rounded-xl py-6 mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold text-neutral-900 dark:text-white mb-2">
{meta.title}
</h1>
<div className="flex flex-wrap items-center text-xs text-neutral-500 dark:text-neutral-400 space-x-4">
{content && (
<div className="flex items-center">
<ClockIcon className="w-4 h-4 mr-1" />
<span>{minuteRead} min read</span>
</div>
)}
</div>
</header>
<div className="prose prose-lg dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeHighlight]}
components={{
a: ({ href, children }) => (
<a
href={href}
className="text-blue-600 dark:text-blue-400 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
img: ({ src, alt }) => (
<img
src={src || ""}
alt={alt || ""}
className="rounded-lg shadow-md w-full h-auto"
/>
),
code: ({
node,
inline,
className,
children,
...props
}: {
node?: any;
inline?: boolean;
className?: string;
children?: React.ReactNode;
[key: string]: any;
}) => {
const match = /language-(\w+)/.exec(className || "");
return !inline && match ? (
<div className="relative">
<pre
className={`${className} rounded-lg p-4 overflow-x-auto`}
>
<code className={`language-${match[1]}`} {...props}>
{children}
</code>
</pre>
<div className="absolute top-2 right-2 text-xs text-neutral-400 dark:text-neutral-500 uppercase">
{match[1]}
</div>
</div>
) : (
<code
className="bg-neutral-100 dark:bg-neutral-800 rounded px-1 py-0.5"
{...props}
>
{children}
</code>
);
},
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-blue-500 pl-4 italic">
{children}
</blockquote>
),
}}
>
{content}
</ReactMarkdown>
</div>
</article>
</div>
);
}
38 changes: 38 additions & 0 deletions app/blogs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getBlogsByYear } from "@/lib/getBlogs";
import { Timeline } from "@/components/ui/timeline";

export default async function BlogsPage() {
const blogsByYear = getBlogsByYear();
//covert object to array
const blogsByYearArray = Object.keys(blogsByYear).map((year) => ({
year,
blogs: blogsByYear[year],
}));

return (
<div className="max-w-3xl mx-auto p-4">
{/* {Object.keys(blogsByYear)
.sort((a, b) => parseInt(b) - parseInt(a))
.map((year) => (
<div key={year} className="mb-8">
<h2 className="text-2xl font-semibold mb-4">{year}</h2>
<ul className="space-y-2">
{blogsByYear[year].map((blog) => (
<li key={blog.slug}>
<Link
href={`/blogs/${blog.slug}`}
className="text-blue-600 hover:underline"
>
{blog.title}
</Link>
</li>
))}
</ul>
</div>
))} */}
<div className="w-full">
<Timeline data={blogsByYearArray} />
</div>
</div>
);
}
24 changes: 24 additions & 0 deletions components/BlogListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import { motion } from 'framer-motion'
import Link from 'next/link'
import { getBlogsByYear } from '@/lib/getBlogs'

export function BlogListItem({ blog, index }: { blog: any; index: number }) {
return (
<motion.li
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Link
href={`/blogs/${blog.slug}`}
className="block p-4 rounded-lg bg-card text-card-foreground shadow-sm hover:shadow-md transition-shadow"
>
<h3 className="text-lg font-semibold mb-2">{blog.title}</h3>
<p className="text-sm text-muted-foreground">{blog.date}</p>
</Link>
</motion.li>
)
}

48 changes: 48 additions & 0 deletions components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client"

import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

import { cn } from "@/lib/utils"

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }
115 changes: 115 additions & 0 deletions components/ui/timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"use client";

import {
useScroll,
useTransform,
motion,
AnimatePresence,
} from "framer-motion";
import Link from "next/link";
import React, { useEffect, useRef, useState } from "react";
import Markdown from "react-markdown";

interface Blog {
title: string;
content: string;
slug: string;
}

interface YearlyBlogs {
year: string;
blogs: Blog[];
}

export const Timeline = ({ data }: { data: YearlyBlogs[] }) => {
const ref = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);

useEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setHeight(rect.height);
}
}, [ref]);

const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start 10%", "end 50%"],
});

const heightTransform = useTransform(scrollYProgress, [0, 1], [0, height]);
const opacityTransform = useTransform(scrollYProgress, [0, 0.1], [0, 1]);

return (
<div
className="w-full bg-white dark:bg-neutral-950 font-sans md:px-10"
ref={containerRef}
>
<div className="max-w-7xl mx-auto py-20 px-4 md:px-8 lg:px-10">
<h2 className="text-3xl font-semibold md:text-5xl md:mb-4 text-black dark:text-white max-w-4xl">
Leetcode Journal
</h2>
<p className="text-neutral-700 dark:text-neutral-300 text-xs md:text-base max-w-sm">
Documenting my progress and learnings from solving Leetcode problems.
</p>
</div>

<div ref={ref} className="relative max-w-7xl mx-auto pb-20">
{data.map((item, yearIndex) => (
<div key={item.year}>
{item.blogs.map((blog, blogIndex) => (
<div
key={blog.slug}
className="flex justify-start pt-10 md:pt-40 md:gap-10"
>
<div className="sticky flex flex-col md:flex-row z-40 items-center top-40 self-start max-w-xs lg:max-w-sm md:w-full">
<div className="h-10 absolute left-3 md:left-3 w-10 rounded-full bg-white dark:bg-black flex items-center justify-center">
<div className="h-4 w-4 rounded-full bg-neutral-200 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 p-2" />
</div>
<h3 className="hidden md:block text-xl md:pl-20 md:text-5xl font-bold text-neutral-500 dark:text-neutral-500">
{blogIndex === 0 ? item.year : ""}
</h3>
</div>

<div className="relative pl-20 pr-4 md:pl-4 w-full">
<Link
href={`/blogs/${blog.slug}`}
className="md:block text-2xl mb-4 text-left font-bold text-neutral-700 dark:text-neutral-300 cursor-pointer hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{blog.title}
</Link>
<div className="overflow-hidden">
<div className={"line-clamp-6"}>
<Markdown>{blog.content}</Markdown>
</div>
</div>
<Link
href={`/blogs/${blog.slug}`}
className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 text-sm mt-2 focus:outline-none"
>
Read more
</Link>
</div>
</div>
))}
</div>
))}
<div
style={{
height: height + "px",
}}
className="absolute md:left-8 left-8 top-0 overflow-hidden w-[2px] bg-[linear-gradient(to_bottom,var(--tw-gradient-stops))] from-transparent from-[0%] via-neutral-200 dark:via-neutral-700 to-transparent to-[99%] [mask-image:linear-gradient(to_bottom,transparent_0%,black_10%,black_90%,transparent_100%)] "
>
<motion.div
style={{
height: heightTransform,
opacity: opacityTransform,
}}
className="absolute inset-x-0 top-0 w-[2px] bg-gradient-to-t from-purple-500 via-blue-500 to-transparent from-[0%] via-[10%] rounded-full"
/>
</div>
</div>
</div>
);
};
Loading
Loading