+ {frontmatter.title}
+
+
+
+ ·
+
+
+
+
+ {frontmatter.tags?.map((tag) => (
+
+ ))}
+
+
+ {frontmatter.description}
+
+ {children}
+
+ );
+};
diff --git a/src/components/boards/BoardPreview/BoardPreview.tsx b/src/components/boards/BoardPreview/BoardPreview.tsx
index 02d89b7..2ddcc05 100644
--- a/src/components/boards/BoardPreview/BoardPreview.tsx
+++ b/src/components/boards/BoardPreview/BoardPreview.tsx
@@ -2,14 +2,14 @@ import Image, { StaticImageData } from "next/image";
import Link from "next/link";
import styles from "./BoardPreview.module.scss";
-type BoardPreview = {
+type BoardPreviewProps = {
title: string;
description: string;
image: StaticImageData;
url: string;
};
-export const BoardPreview = ({ title, description, image, url }: BoardPreview) => {
+export const BoardPreview = ({ title, description, image, url }: BoardPreviewProps) => {
return (
diff --git a/src/components/common/AnimatedArrow/AnimatedArrow.module.scss b/src/components/common/AnimatedArrow/AnimatedArrow.module.scss
new file mode 100644
index 0000000..6b9913b
--- /dev/null
+++ b/src/components/common/AnimatedArrow/AnimatedArrow.module.scss
@@ -0,0 +1,37 @@
+.animatedArrow {
+ display: flex;
+ margin-left: 2px;
+ width: 0.75em;
+
+ .arrowIcon {
+ width: 100%;
+ margin-left: 3px;
+ overflow: visible;
+ }
+
+ .arrowHead {
+ transform: translateX(0);
+ transition: transform 0.1s ease-in-out;
+ }
+
+ .arrowBody {
+ opacity: 0;
+ transform: scaleX(1);
+ transition:
+ transform 0.1s ease-in-out,
+ opacity 0.1s ease-in-out;
+ }
+}
+
+// These classes need to be applied to a parent element
+// that will trigger the hover effect
+:global(.arrow-hover-trigger):hover {
+ .arrowHead {
+ transform: translateX(3px);
+ }
+
+ .arrowBody {
+ opacity: 1;
+ transform: scaleX(2);
+ }
+}
diff --git a/src/components/common/AnimatedArrow/AnimatedArrow.tsx b/src/components/common/AnimatedArrow/AnimatedArrow.tsx
new file mode 100644
index 0000000..cf8bb63
--- /dev/null
+++ b/src/components/common/AnimatedArrow/AnimatedArrow.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import clsx from "clsx";
+import styles from "./AnimatedArrow.module.scss";
+
+type AnimatedArrowProps = {
+ className?: string;
+ color?: string;
+};
+
+export const AnimatedArrow = ({ className = "", color = "currentColor" }: AnimatedArrowProps) => {
+ return (
+
+ );
+};
diff --git a/src/components/common/Header/Header.tsx b/src/components/common/Header/Header.tsx
index b803255..b100935 100644
--- a/src/components/common/Header/Header.tsx
+++ b/src/components/common/Header/Header.tsx
@@ -5,6 +5,8 @@ import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import styles from "./Header.module.scss";
+const NAV_LINKS = ["/stuff", "/uses", "/blog"];
+
export const Header = () => {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
@@ -25,16 +27,13 @@ export const Header = () => {
/
-
-
- /stuff
-
-
-
-
- /uses
-
-
+ {NAV_LINKS.map((link) => (
+
+
+ {link}
+
+
+ ))}
-
diff --git a/src/content/hello-world.mdx b/src/content/hello-world.mdx
new file mode 100644
index 0000000..2b36b0d
--- /dev/null
+++ b/src/content/hello-world.mdx
@@ -0,0 +1,17 @@
+---
+title: Hello World
+date: "2024-01-30"
+description: This is my first blog post using the new MDX system
+tags: ["blog", "mdx", "one"]
+---
+
+# Hello World
+
+This is an example blog post written in MDX. You can use **Markdown** syntax and also import and use React components.
+
+## Features
+
+- MDX Support
+- Frontmatter metadata
+- Syntax highlighting
+- React components
diff --git a/src/content/hello-world2.mdx b/src/content/hello-world2.mdx
new file mode 100644
index 0000000..a5e8bb2
--- /dev/null
+++ b/src/content/hello-world2.mdx
@@ -0,0 +1,17 @@
+---
+title: Hello World 2
+date: "2024-01-30"
+description: This is my second blog post using the new MDX system
+tags: ["blog", "mdx", "two"]
+---
+
+# Hello World
+
+This is an example blog post written in MDX. You can use **Markdown** syntax and also import and use React components.
+
+## Features
+
+- MDX Support
+- Frontmatter metadata
+- Syntax highlighting
+- React components
diff --git a/src/lib/blog.ts b/src/lib/blog.ts
new file mode 100644
index 0000000..d9ee188
--- /dev/null
+++ b/src/lib/blog.ts
@@ -0,0 +1,67 @@
+import fs from "fs";
+import matter from "gray-matter";
+import path from "path";
+import readingTime from "reading-time";
+import type { BlogPost, BlogPostFrontmatter, BlogPostPreview } from "@/types/blog";
+
+const POSTS_PATH = path.join(process.cwd(), "src/content");
+const POST_FILE_EXT_REGEX = /\.mdx$/;
+
+const getPostFromFile = (
+ file: string,
+ preview: T,
+): T extends true ? BlogPostPreview : BlogPost => {
+ const source = fs.readFileSync(path.join(POSTS_PATH, file), "utf8");
+ const slug = file.replace(POST_FILE_EXT_REGEX, "");
+ const { data, content } = matter(source);
+ const rt = readingTime(content);
+
+ return {
+ slug,
+ readingTime: {
+ text: rt.text,
+ ms: rt.time,
+ },
+ frontmatter: data as BlogPostFrontmatter,
+ ...(preview ? {} : { content }),
+ } as T extends true ? BlogPostPreview : BlogPost;
+};
+
+const sortByDateDesc = (a: BlogPostPreview, b: BlogPostPreview) => {
+ return new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime();
+};
+
+export function getAllPosts(): BlogPostPreview[] {
+ try {
+ const files = fs.readdirSync(POSTS_PATH);
+ const mdxFiles = files.filter((file) => POST_FILE_EXT_REGEX.test(file));
+ const posts = mdxFiles.map((file) => getPostFromFile(file, true));
+ const postsSorted = posts.sort(sortByDateDesc);
+
+ return postsSorted;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error reading posts:", error);
+ return [];
+ }
+}
+
+export function getAllTags(posts: BlogPostPreview[]): string[] {
+ const tags = new Set();
+
+ posts.forEach((post) => {
+ post.frontmatter.tags?.forEach((tag) => {
+ tags.add(tag);
+ });
+ });
+
+ return Array.from(tags);
+}
+
+export function getPostBySlug(slug: string): BlogPost | null {
+ try {
+ return getPostFromFile(`${slug}.mdx`, false);
+ } catch {
+ return null;
+ }
+}
diff --git a/src/lib/date.ts b/src/lib/date.ts
new file mode 100644
index 0000000..3b4410e
--- /dev/null
+++ b/src/lib/date.ts
@@ -0,0 +1,25 @@
+import { Duration, format, formatISODuration, intervalToDuration } from "date-fns";
+import { ReadingTime } from "@/types/blog";
+
+export const formatBlogPostDate = (date: string) => {
+ return format(new Date(date), "MMMM d, yyyy");
+};
+
+const createDurationFromMilliseconds = (milliseconds: number): Duration => {
+ const interval = {
+ start: new Date(0),
+ end: new Date(milliseconds),
+ };
+
+ const duration = intervalToDuration(interval);
+
+ return duration;
+};
+
+export const getReadingTimeISODuration = (readingTime: ReadingTime) => {
+ const duration = createDurationFromMilliseconds(readingTime.ms);
+
+ const formattedDuration = formatISODuration(duration);
+
+ return formattedDuration;
+};
diff --git a/src/lib/url.ts b/src/lib/url.ts
new file mode 100644
index 0000000..92de8ff
--- /dev/null
+++ b/src/lib/url.ts
@@ -0,0 +1 @@
+export const getHash = () => (typeof window !== "undefined" ? window.location.hash : undefined);
diff --git a/src/types/blog.ts b/src/types/blog.ts
new file mode 100644
index 0000000..14648b1
--- /dev/null
+++ b/src/types/blog.ts
@@ -0,0 +1,36 @@
+/**
+ * Represents the frontmatter structure for a blog post.
+ *
+ * @interface BlogPostFrontmatter
+ * @property {string} title - The title of the blog post
+ * @property {string} date - The publication date in ISO 8601 format (e.g., "2023-04-21")
+ * @property {string} description - A brief description or summary of the blog post
+ * @property {string[]} [tags] - Optional array of tags associated with the blog post
+ */
+export type BlogPostFrontmatter = {
+ title: string;
+ // ISO 8601 date format
+ date: string;
+ description: string;
+ tags?: string[];
+};
+
+/**
+ * Represents the estimated reading time for a blog post.
+ * @interface ReadingTime
+ * @property {string} text - Human-readable representation of the reading time (e.g., "3 min read").
+ * @property {number} ms - Reading time in milliseconds.
+ */
+export type ReadingTime = {
+ text: string;
+ ms: number;
+};
+
+export type BlogPost = {
+ slug: string;
+ content: string;
+ readingTime: ReadingTime;
+ frontmatter: BlogPostFrontmatter;
+};
+
+export type BlogPostPreview = Omit;
diff --git a/tsconfig.json b/tsconfig.json
index 972dda1..ce1814d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,6 +24,12 @@
}
]
},
- "include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"],
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ "src/components/blog/post/PostLayout"
+ ],
"exclude": ["node_modules"]
}