Skip to content

Commit ab5f0e8

Browse files
committed
Make layout for blog posts
1 parent bc3eb85 commit ab5f0e8

28 files changed

+622
-195
lines changed

package-lock.json

Lines changed: 287 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@hookform/resolvers": "^2.9.11",
1818
"class-variance-authority": "^0.4.0",
1919
"framer-motion": "^9.1.7",
20+
"linkedom": "^0.14.25",
2021
"next": "13.2.4",
2122
"nextjs-google-analytics": "^2.3.3",
2223
"react": "18.2.0",
@@ -42,4 +43,4 @@
4243
"postcss": "^8.4.21",
4344
"tailwindcss": "^3.2.7"
4445
}
45-
}
46+
}

scripts/generateArticlesIndex.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ const run = async () => {
2020
.forEach((articleFilename, i, arr) => {
2121
const importName = `article${i + 1}`;
2222
fileImports += `import ${importName} from "./${articleFilename}";\n`;
23-
exportArrayContent += `\t${importName}${arr.length - 1 === i ? "" : ","}\n`;
23+
exportArrayContent += `\t{ \n\t\tfilename: "${articleFilename}", \n\t\tarticle: ${importName} \n\t}${arr.length - 1 === i ? "" : ","}\n`;
2424
});
2525

26-
exportArrayContent += "] as IArticle[];";
26+
exportArrayContent += "] as { filename: string; article: IArticle }[];";
2727

2828
const indexContent = `${fileImports}\n${exportArrayContent}`;
2929
await fs.writeFile(indexPath, indexContent);

src/assets/state/articles/1.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { people } from "../team";
33
import tags from "./tags";
44

55
import thumbnail from "@/assets/images/content/blog/1/thumbnail.webp";
6+
import ArticleTableOfContent from "@/components/pages/blog/post/ArticleTableOfContent";
67

78
export default {
89
title: "1",
@@ -22,7 +23,16 @@ export default {
2223

2324
teaser: "Lorem ipsum dolor sit amet consectetur Voluptates facere quasi repellat doloremque quae saepe?",
2425
content: <>
25-
1
26+
<h2>Heading 2</h2>
27+
<h3>Heading 3</h3>
28+
<h4>Heading 4</h4>
29+
<h5>Heading 5</h5>
30+
31+
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Aut quia facilis dolor ullam distinctio tempora delectus laudantium similique. Eos mollitia maxime nam id nemo repellendus natus accusamus dicta quam illum.
32+
<ArticleTableOfContent />
33+
34+
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Rem aliquam illum aut atque tempore quod repudiandae ad maiores molestias? Maxime animi at incidunt omnis rem nostrum, ipsum ab molestias deleniti.
35+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum recusandae laborum fugit quo iusto culpa animi ab cupiditate cumque. Labore modi rem, enim molestias sint eaque porro velit facilis excepturi?
2636
</>,
2737

2838
created: new Date("2023/03/24")

src/assets/state/articles/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import article1 from "./1";
44
import article2 from "./2";
55

66
export default [
7-
article1,
8-
article2
9-
] as IArticle[];
7+
{
8+
filename: "1",
9+
article: article1
10+
},
11+
{
12+
filename: "2",
13+
article: article2
14+
}
15+
] as { filename: string; article: IArticle }[];

src/assets/state/roadmap.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Heading from "@/components/layout/heading";
1+
import Heading from "@/components/layout/Heading";
22
import Link from "@/components/navigation/Link";
33
import { ReactNode } from "react";
44

@@ -83,4 +83,4 @@ export default [
8383
</p>
8484
</>
8585
}
86-
]satisfies IRoadmapItem[];
86+
] satisfies IRoadmapItem[];

src/components/pages/blog/ArticleBrief.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const ArticleBrief = ({ title, thumbnail, thumbnailAlt, readtime, teaser, author
5050

5151
return (
5252
<motion.li
53-
className="origin-center flex flex-col flex-1 gap-4 mx-0 rounded-lg motion-safe:transition-[margin-inline] motion-safe:duration-500 sm:mx-16 md:mx-0 bg-primary/20"
53+
className="origin-center flex flex-col flex-1 gap-4 mx-0 rounded-lg motion-safe:transition-[margin-inline] motion-safe:duration-500 sm:mx-16 md:mx-0 bg-primary-300"
5454
variants={listAnim}
5555
initial="in"
5656
animate="anim"
@@ -72,17 +72,17 @@ const ArticleBrief = ({ title, thumbnail, thumbnailAlt, readtime, teaser, author
7272
<LinkButton
7373
key={i}
7474
href={makeTagUrl(tag)}
75+
color="secondary"
7576
className="px-2 py-1 text-xs font-semibold border"
76-
color="primary"
7777
>
7878
{tag}
7979
</LinkButton>
8080
))}
8181
</div>}
82-
<Link href={url} color="fill-contrast" className="block text-lg font-bold">
82+
<Link href={url} color="fill-contrast" className="block text-2xl font-bold">
8383
{title}
8484
</Link>
85-
<Link href={url} color="fill-contrast" className="flex-1">
85+
<Link href={url} color="fill-contrast" className="flex-1 ">
8686
{teaser}
8787
</Link>
8888
<div className="flex flex-col flex-wrap gap-4 pt-4 mt-auto">
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import IArticle from "@/assets/state/articles/article";
2+
import { createContext, useContext } from "react";
3+
4+
export const ArticleContext = createContext<ArticleContext>({ ready: false });
5+
6+
export type ArticleContext = ({
7+
ready: true;
8+
headings: {
9+
text: string;
10+
level: number;
11+
id: string;
12+
}[];
13+
path: string;
14+
} & IArticle) | {
15+
ready: false;
16+
};
17+
18+
export const useArticle = () => useContext(ArticleContext);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from "react";
2+
import BookIcon from "@heroicons/react/24/outline/BookOpenIcon";
3+
import CalendarIcon from "@heroicons/react/24/solid/CalendarIcon";
4+
5+
import { IMember } from "@/assets/state/team";
6+
7+
import useSSGSafe from "@/hooks/useSSGSafe";
8+
9+
import Link from "@/components/navigation/Link";
10+
import { readtimeFormatter } from "@/utils/readtime";
11+
12+
export interface ArticleMetaProps {
13+
author: IMember;
14+
readtime: number;
15+
created: string;
16+
updated?: string | null;
17+
}
18+
19+
const DotSeparator = <div className="hidden w-1 h-1 rounded-full bg-neutral-600 md:block" />;
20+
21+
const ArticleMeta = ({ author, readtime, created, updated }: ArticleMetaProps) => {
22+
const safeToRender = useSSGSafe();
23+
const AuthorTag = author.links.length > 0 ? Link : "div";
24+
25+
return (
26+
<div className="flex flex-wrap items-center justify-center gap-4">
27+
<AuthorTag
28+
className="flex items-center gap-2 group/author"
29+
//@ts-ignore
30+
href={author.links.length > 0 ? author.links[0].href : undefined}
31+
//@ts-ignore
32+
color={author.links.length > 0 ? "fill-contrast" : undefined}
33+
data-is-link={author.links.length > 0}
34+
>
35+
<div className="overflow-hidden rounded-full">
36+
<img
37+
className="object-contain w-8 h-8 rounded-full aspect-square group-hover/author:scale-110 motion-safe:transition-all"
38+
src={author.image.src}
39+
width={author.image.width}
40+
height={author.image.height}
41+
alt="A picture of the author"
42+
/>
43+
</div>
44+
<div className="flex flex-col">
45+
<div>{author.fullName}</div>
46+
<div className="text-sm">{author.title}</div>
47+
</div>
48+
</AuthorTag>
49+
{DotSeparator}
50+
<div className="flex items-center gap-4 text-sm">
51+
{safeToRender && <div className="flex items-center gap-2">
52+
<BookIcon className="w-4 h-4" />
53+
<time
54+
aria-label="reading time"
55+
dateTime={`${readtime} minutes`}
56+
>
57+
{readtimeFormatter.format(readtime)}
58+
</time>
59+
</div>}
60+
{DotSeparator}
61+
{safeToRender && <div className="flex items-center gap-2">
62+
<CalendarIcon className="w-4 h-4" />
63+
<time aria-label="Date created" dateTime={created} className={`${updated ? "hidden" : ""}`}>
64+
{(new Date(created)).toLocaleDateString()}
65+
</time>
66+
{updated && <time aria-label="Date updated" dateTime={updated}>
67+
{(new Date(updated)).toLocaleDateString()}
68+
</time>}
69+
</div>}
70+
</div>
71+
</div>
72+
);
73+
};
74+
75+
export default ArticleMeta;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Link from "@/components/navigation/Link";
2+
import { useArticle } from "./ArticleContext";
3+
4+
export interface TableOfContentProps {
5+
/**
6+
* Decides the minimum level a heading should have to appear in the table of contents
7+
* @default 2
8+
*/
9+
minLevel?: number;
10+
11+
/**
12+
* Decides the maximum level a heading should have to appear in the table of contents
13+
* @default 4
14+
*/
15+
maxLevel?: number;
16+
}
17+
18+
const ArticleTableOfContent = ({ minLevel = 2, maxLevel = 4 }: TableOfContentProps) => {
19+
20+
const article = useArticle();
21+
22+
23+
return (
24+
<ul>
25+
{article.ready && article.headings.map((heading, i) => {
26+
if (heading.level < minLevel || heading.level > maxLevel) return null;
27+
28+
const url = new URL(article.path, "http://example.com");
29+
url.hash = heading.id;
30+
31+
return <li key={i}>
32+
<Link color="primary" href={url.toString().replace("http://example.com", "")} underline>
33+
{heading.text}
34+
</Link>
35+
</li>;
36+
})}
37+
</ul>
38+
);
39+
};
40+
41+
export default ArticleTableOfContent;

0 commit comments

Comments
 (0)