diff --git a/scripts/generate-rss.js b/scripts/generate-rss.js index 50d9bcc..286f43d 100644 --- a/scripts/generate-rss.js +++ b/scripts/generate-rss.js @@ -6,6 +6,7 @@ const { format } = require("date-fns"); const xmlFormat = require("xml-formatter"); const BLOGS_DIR = path.join(process.cwd(), "_blogs"); +const CASE_STUDIES_DIR = path.join(process.cwd(), "_case-studies"); const OUTPUT_FILE = path.join(process.cwd(), "public", "rss.xml"); function escapeXML(str) { @@ -18,9 +19,30 @@ function escapeXML(str) { } function generateRSS() { - const files = fs.readdirSync(BLOGS_DIR); + let blogFiles = []; + let caseStudyFiles = []; - const posts = files.map((file) => { + try { + if (fs.existsSync(BLOGS_DIR)) { + blogFiles = fs.readdirSync(BLOGS_DIR); + } else { + console.warn("Blogs directory does not exist:", BLOGS_DIR); + } + } catch (error) { + console.error("Error reading blogs directory:", error); + } + + try { + if (fs.existsSync(CASE_STUDIES_DIR)) { + caseStudyFiles = fs.readdirSync(CASE_STUDIES_DIR); + } else { + console.warn("Case studies directory does not exist:", CASE_STUDIES_DIR); + } + } catch (error) { + console.error("Error reading case studies directory:", error); + } + + const posts = blogFiles.map((file) => { const filePath = path.join(BLOGS_DIR, file); const content = fs.readFileSync(filePath, "utf-8"); const { data } = matter(content); @@ -32,7 +54,19 @@ function generateRSS() { }; }); - const rssItems = posts + const caseStudies = caseStudyFiles.map((file) => { + const filePath = path.join(CASE_STUDIES_DIR, file); + const content = fs.readFileSync(filePath, "utf-8"); + const { data } = matter(content); + return { + title: data.title, + description: data.excerpt, + link: `https://umairjibran.com/case-studies/${file.replace(/\.md$/, "")}`, + pubDate: data.date, + }; + }); + + const rssItems = [...posts, ...caseStudies] .map( (post) => `${escapeXML(post.title)}${escapeXML(post.link)}${escapeXML(post.description)}${new Date(post.pubDate).toUTCString()}${post.link}`, @@ -40,7 +74,7 @@ function generateRSS() { .join(""); const rssFeed = - `Umair Jibran's Bloghttps://umairjibran.com/blogsLatest articles from Umair Jibran's blog.en-us${new Date().toUTCString()}${rssItems}`.trim(); + `Umair Jibran's Writingshttps://umairjibran.com/writingsLatest articles from Umair Jibran's writings.en-us${new Date().toUTCString()}${rssItems}`.trim(); fs.writeFileSync(OUTPUT_FILE, xmlFormat(rssFeed), "utf-8"); console.log(`RSS feed generated at ${OUTPUT_FILE}`); diff --git a/src/app/(blogs)/blogs/[slug]/page.tsx b/src/app/(blogs)/blogs/[slug]/page.tsx deleted file mode 100644 index 19e898c..0000000 --- a/src/app/(blogs)/blogs/[slug]/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Metadata } from "next"; -import { notFound } from "next/navigation"; -import { getAllBlog, getBlogBySlug } from "@/lib/api"; -import markdownToHtml from "@/lib/markdownToHtml"; -import { StoryBody } from "@/components/StoryBody"; -import { StoryHeader } from "@/components/StoryHeader"; - -import profile from "@/data/profile.json"; - -export default async function Blog({ params }: Params) { - const blog = getBlogBySlug(params.slug); - - if (!blog) { - return notFound(); - } - - const content = await markdownToHtml(blog.content || ""); - - return ( -
-
- - -
-
- ); -} - -type Params = { - params: { - slug: string; - }; -}; - -export function generateMetadata({ params }: Params): Metadata { - const blog = getBlogBySlug(params.slug); - - if (!blog) { - return notFound(); - } - - const title = `${blog.title} | Blog | ${profile.name.firstName} ${profile.name.lastName}`; - - return { - title, - openGraph: { - title, - images: [blog.ogImage.url], - }, - twitter: { - card: "summary_large_image", - }, - description: blog.excerpt, - authors: [blog.author], - keywords: blog.tags, - }; -} - -export async function generateStaticParams() { - const blogs = getAllBlog(); - - return blogs.map((blog) => ({ - slug: blog.slug, - })); -} diff --git a/src/app/(blogs)/blogs/page.tsx b/src/app/(blogs)/blogs/page.tsx deleted file mode 100644 index b8ec770..0000000 --- a/src/app/(blogs)/blogs/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { HeroStory } from "@/components/HeroStory"; -import { MoreStories } from "@/components/MoreStories"; -import { getAllBlog } from "@/lib/api"; - -export default function Index() { - const allBlogs = getAllBlog(); - const heroBlog = allBlogs[0]; - const moreBlogs = allBlogs.slice(1); - - if (allBlogs.length === 0) { - return ( -
-
oops... I should write some up
-
- ); - } - - return ( -
- - {moreBlogs.length > 0 && } -
- ); -} diff --git a/src/app/(blogs)/layout.tsx b/src/app/(blogs)/layout.tsx deleted file mode 100644 index 20f0eba..0000000 --- a/src/app/(blogs)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Metadata } from "next"; -import profile from "@/data/profile.json"; -import meta from "@/data/meta.json"; - -export const metadata: Metadata = { - ...meta, - metadataBase: new URL(meta.metadataBase), - title: `Thoughts and blogs by ${profile.name.firstName} ${profile.name.lastName}`, - description: `Blogs authored by ${profile.name.firstName} ${profile.name.lastName}`, -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return
{children}
; -} diff --git a/src/app/(caseStudies)/case-studies/[slug]/page.tsx b/src/app/(caseStudies)/case-studies/[slug]/page.tsx deleted file mode 100644 index 91490c5..0000000 --- a/src/app/(caseStudies)/case-studies/[slug]/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Metadata } from "next"; -import { notFound } from "next/navigation"; -import { getAllCaseStudies, getCaseStudyBySlug } from "@/lib/api"; -import markdownToHtml from "@/lib/markdownToHtml"; -import { StoryBody } from "@/components/StoryBody"; -import { StoryHeader } from "@/components/StoryHeader"; - -import profile from "@/data/profile.json"; - -export default async function CaseStudy({ params }: Params) { - const caseStudy = getCaseStudyBySlug(params.slug); - - if (!caseStudy) { - return notFound(); - } - - const content = await markdownToHtml(caseStudy.content || ""); - - return ( -
-
- - -
-
- ); -} - -type Params = { - params: { - slug: string; - }; -}; - -export function generateMetadata({ params }: Params): Metadata { - const CaseStudy = getCaseStudyBySlug(params.slug); - - if (!CaseStudy) { - return notFound(); - } - - const title = `${CaseStudy.title} | Case Study | ${profile.name.firstName} ${profile.name.lastName}`; - - return { - title, - openGraph: { - title, - images: [CaseStudy.ogImage.url], - }, - twitter: { - card: "summary_large_image", - }, - description: CaseStudy.excerpt, - authors: [CaseStudy.author], - keywords: CaseStudy.tags, - }; -} - -export async function generateStaticParams() { - const caseStudies = getAllCaseStudies(); - - return caseStudies.map((caseStudy) => ({ - slug: caseStudy.slug, - })); -} diff --git a/src/app/(caseStudies)/case-studies/page.tsx b/src/app/(caseStudies)/case-studies/page.tsx deleted file mode 100644 index 62a4448..0000000 --- a/src/app/(caseStudies)/case-studies/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { HeroStory } from "@/components/HeroStory"; -import { MoreStories } from "@/components/MoreStories"; -import { getAllCaseStudies } from "@/lib/api"; - -export default function Index() { - const allCaseStudies = getAllCaseStudies(); - - if (allCaseStudies.length === 0) { - return ( -
-
oops... I should write some up
-
- ); - } - - const heroCaseStudy = allCaseStudies[0]; - const moreCaseStudies = allCaseStudies.slice(1); - - return ( -
- - {moreCaseStudies.length > 0 && } -
- ); -} diff --git a/src/app/(caseStudies)/layout.tsx b/src/app/(caseStudies)/layout.tsx deleted file mode 100644 index 96c31a5..0000000 --- a/src/app/(caseStudies)/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Metadata } from "next"; -import profile from "@/data/profile.json"; -import meta from "@/data/meta.json"; - -export const metadata: Metadata = { - ...meta, - metadataBase: new URL(meta.metadataBase), - title: `Case Studies by ${profile.name.firstName} ${profile.name.lastName}`, - description: `Case Studies authored by ${profile.name.firstName} ${profile.name.lastName}`, -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return
{children}
; -} diff --git a/src/app/writing/[slug]/page.tsx b/src/app/writing/[slug]/page.tsx new file mode 100644 index 0000000..0586072 --- /dev/null +++ b/src/app/writing/[slug]/page.tsx @@ -0,0 +1,73 @@ +import { getAllBlog, getAllCaseStudies } from "@/lib/api"; +import { notFound } from "next/navigation"; +import markdownToHtml from "@/lib/markdownToHtml"; +import { StoryBody } from "@/components/StoryBody"; +import { RelatedStories } from "@/components/RelatedStories"; + +export default async function StoryPage({ params }: { params: { slug: string } }) { + const allStories = [ + ...getAllBlog(), + ...getAllCaseStudies() + ]; + + // Find the story in either blogs or case studies + const story = allStories.find(story => story.slug === params.slug); + + if (!story) { + notFound(); + } + + const content = await markdownToHtml(story.content || ""); + + return ( +
+ {/* Header with gradient */} +
+
+
+
+ + {story.type.replace("-", " ")} + + +
+

{story.title}

+

{story.excerpt}

+
+
+
+ + {/* Content with sidebar */} +
+
+ {/* Main content */} +
+ +
+ + {/* Sidebar */} +
+
+ +
+
+
+
+
+ ); +} + +export async function generateStaticParams() { + const allContent = [ + ...getAllBlog(), + ...getAllCaseStudies() + ]; + + return allContent.map((story) => ({ + slug: story.slug, + })); +} \ No newline at end of file diff --git a/src/app/writing/page.tsx b/src/app/writing/page.tsx new file mode 100644 index 0000000..3d4d7e4 --- /dev/null +++ b/src/app/writing/page.tsx @@ -0,0 +1,52 @@ +import { MoreStories } from "@/components/MoreStories"; +import { getAllBlog, getAllCaseStudies } from "@/lib/api"; +import Link from "next/link"; + +export default function Index() { + // Combine and sort all content + const allContent = [ + ...getAllBlog(), + ...getAllCaseStudies() + ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + if (allContent.length === 0) { + return ( +
+
oops... I should write some up
+
+ ); + } + + return ( +
+ {/* Header section with gradient background */} +
+
+

Writing

+
+

+ I write about software engineering, web development, and the technical challenges I encounter. + Here you will find in-depth case studies, technical articles, and development stories. +

+

+ Subscribe via{" "} + + RSS + + {" "}or follow me on{" "} + + LinkedIn + + {" "}to stay updated. +

+
+
+
+ + {/* All content */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/CoverImage.tsx b/src/components/CoverImage.tsx index 20604ca..8caa5b2 100644 --- a/src/components/CoverImage.tsx +++ b/src/components/CoverImage.tsx @@ -1,23 +1,27 @@ import cn from "classnames"; import Link from "next/link"; import Image from "next/image"; +import { CSSProperties } from "react"; type Props = { title: string; src: string; slug?: string; + className?: string; + style?: CSSProperties; }; -const CoverImage = ({ title, src, slug }: Props) => { +const CoverImage = ({ title, src, slug, className, style }: Props) => { const image = ( {`Cover ); return ( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index da729c0..f2ba89f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -18,14 +18,8 @@ export default function Header() { ); navComponents.push( - - Blogs - , - ); - - navComponents.push( - - Case Studies + + Writing , ); diff --git a/src/components/MoreStories.tsx b/src/components/MoreStories.tsx index 7f1e2fd..37299ce 100644 --- a/src/components/MoreStories.tsx +++ b/src/components/MoreStories.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Story } from "@/types/story"; import { StoryPreview } from "@/components/StoryPreview"; @@ -5,25 +7,93 @@ type Props = { stories: Story[]; }; +type GroupedStories = { + [key: string]: { + month: string; + year: string; + stories: Story[]; + }; +}; + export function MoreStories({ stories }: Props) { + // Group stories by month and year + const groupedStories = stories.reduce((acc: GroupedStories, story) => { + const date = new Date(story.date); + const key = `${date.getFullYear()}-${String(date.getMonth()).padStart(2, '0')}`; + const month = date.toLocaleString('default', { month: 'long' }); + const year = date.getFullYear().toString(); + + if (!acc[key]) { + acc[key] = { + month, + year, + stories: [] + }; + } + acc[key].stories.push(story); + return acc; + }, {}); + + // Sort stories within each month by date (newest first) + Object.values(groupedStories).forEach(group => { + group.stories.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + }); + + // Sort keys in reverse chronological order + const sortedKeys = Object.keys(groupedStories).sort().reverse(); + + // Calculate the height for the last section (last month's stories) + const lastMonthKey = sortedKeys[sortedKeys.length - 1]; + const lastMonthStoriesCount = groupedStories[lastMonthKey]?.stories.length || 0; + const lastSectionHeight = lastMonthStoriesCount * 96 + 24; // height per story + padding + return ( -
-

- More Stories -

-
- {stories.map((story) => ( - - ))} +
+
+ {/* Main continuous timeline */} +
+ +
+ {sortedKeys.map((key, groupIndex) => ( +
+ {/* Month marker */} +
+
+ {/* Month bullet */} +
+ {/* Month label */} +
+

+ {groupedStories[key].month} {groupedStories[key].year} +

+
+
+ +
+ {groupedStories[key].stories.map((story) => ( +
+ +
+ ))} +
+
+ ))} +
); diff --git a/src/components/RelatedStories.tsx b/src/components/RelatedStories.tsx new file mode 100644 index 0000000..1748af1 --- /dev/null +++ b/src/components/RelatedStories.tsx @@ -0,0 +1,102 @@ +import { Story } from "@/types/story"; +import Link from "next/link"; + +type Props = { + currentStory: Story; + allStories: Story[]; +}; + +type RelatedStoryResult = { + story: Story; + matchingTag?: string; +}; + +function findRelatedStory(stories: Story[], currentStory: Story, type: "blog" | "case-study"): RelatedStoryResult | null { + // Filter stories by type and exclude current story + const typeStories = stories.filter(s => s.type === type && s.slug !== currentStory.slug); + if (typeStories.length === 0) return null; + + // Try to find a story with matching tags + if (currentStory.tags && currentStory.tags.length > 0) { + const relatedByTags = typeStories.map(story => { + const matchingTags = story.tags?.filter(tag => currentStory.tags.includes(tag)) || []; + return { + story, + matchingTags, + matchingCount: matchingTags.length + }; + }) + .sort((a, b) => b.matchingCount - a.matchingCount); + + if (relatedByTags[0].matchingCount > 0) { + return { + story: relatedByTags[0].story, + matchingTag: relatedByTags[0].matchingTags[0] + }; + } + } + + // If no matching tags, return the latest story + return { story: typeStories[0] }; +} + +export function RelatedStories({ currentStory, allStories }: Props) { + const relatedBlog = findRelatedStory(allStories, currentStory, "blog"); + const relatedCaseStudy = findRelatedStory(allStories, currentStory, "case-study"); + + if (!relatedBlog && !relatedCaseStudy) return null; + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/StoryBody.tsx b/src/components/StoryBody.tsx index d8cf992..01f2623 100644 --- a/src/components/StoryBody.tsx +++ b/src/components/StoryBody.tsx @@ -7,7 +7,7 @@ type Props = { export function StoryBody({ content }: Props) { return ( -
+
{parse(content)}
); diff --git a/src/components/StoryHeader.tsx b/src/components/StoryHeader.tsx deleted file mode 100644 index 57913d6..0000000 --- a/src/components/StoryHeader.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import Avatar from "@/components/Avatar"; -import CoverImage from "@/components/CoverImage"; -import DateFormatter from "@/components/DateFormatter"; -import { BlogTitle } from "@/components/BlogTitle"; -import { Author } from "@/types/author"; - -type Props = { - title: string; - coverImage: string; - date: string; - author: Author; -}; - -export function StoryHeader({ title, coverImage, date, author }: Props) { - return ( - <> - {title} -
- -
-
- -
-
-
- -
-
- -
-
- - ); -} diff --git a/src/components/StoryPreview.tsx b/src/components/StoryPreview.tsx index 80522c6..b6b2147 100644 --- a/src/components/StoryPreview.tsx +++ b/src/components/StoryPreview.tsx @@ -1,8 +1,7 @@ +'use client'; + import { Author } from "@/types/author"; import Link from "next/link"; -import CoverImage from "@/components/CoverImage"; -import DateFormatter from "@/components/DateFormatter"; -import Avatar from "@/components/Avatar"; type Props = { title: string; @@ -12,6 +11,7 @@ type Props = { author: Author; slug: string; type: "blog" | "case-study"; + tags?: string[]; }; export function StoryPreview({ @@ -22,25 +22,50 @@ export function StoryPreview({ author, slug, type, + tags = [], }: Props) { const root = type === "blog" ? "/blogs" : "/case-studies"; + const day = new Date(date).getDate(); + return ( -
-
- -
-

- - {title} - -

-
- +
+ {/* Left side - metadata */} +
+ {day}
-

- {excerpt} -

- + + {/* Timeline dot */} +
+ + {/* Right side - content */} + +
+
+ + {type.replace("-", " ")} + + {tags.map((tag) => ( + + {tag} + + ))} +
+

+ {title} +

+

+ {excerpt} +

+
+
); } diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..eb97163 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + + // Redirect /blogs and /case-studies to /writing + if (pathname === '/blogs' || pathname === '/case-studies') { + return NextResponse.redirect(new URL('/writing', request.url)) + } + + // Redirect individual blog posts and case studies + if (pathname.startsWith('/blogs/') || pathname.startsWith('/case-studies/')) { + const slug = pathname.split('/').pop() + return NextResponse.redirect(new URL(`/writing/${slug}`, request.url)) + } + + return NextResponse.next() +} + +export const config = { + matcher: [ + '/blogs', + '/blogs/:path*', + '/case-studies', + '/case-studies/:path*', + ], +} \ No newline at end of file