Skip to content

Commit d926920

Browse files
feat: add blog section (codse#208)
1 parent 4665176 commit d926920

File tree

9 files changed

+388
-6
lines changed

9 files changed

+388
-6
lines changed

app/blog/[[...slug]]/page.tsx

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { notFound } from "next/navigation";
4+
import { allBlogs, Doc } from "contentlayer/generated";
5+
import Balancer from "react-wrap-balancer";
6+
7+
import NavMenu from "@/app/docs/[[...slug]]/nav-menu";
8+
import { Mdx } from "@/components/mdx-components";
9+
import { DocsPager } from "@/components/pager";
10+
import { DashboardTableOfContents } from "@/components/toc";
11+
import { badgeVariants } from "@/components/ui/badge";
12+
import { ScrollArea } from "@/components/ui/scroll-area";
13+
import { blogSidebarNav } from "@/config/blog";
14+
import { siteConfig } from "@/config/site";
15+
import { getTableOfContents } from "@/lib/toc";
16+
import { absoluteUrl, cn } from "@/lib/utils";
17+
import { ChevronRightIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
18+
19+
import "@/styles/mdx.css";
20+
import "@/styles/storybook.css";
21+
22+
interface BlogPageProps {
23+
params: {
24+
slug: string[];
25+
};
26+
}
27+
28+
async function getBlogFromParams({ params }: BlogPageProps) {
29+
const slug = params.slug?.join("/") || "";
30+
const blog = allBlogs.find((doc) => doc.slugAsParams === slug);
31+
32+
if (!blog) {
33+
return null;
34+
}
35+
36+
return blog;
37+
}
38+
39+
export async function generateMetadata({ params }: BlogPageProps): Promise<Metadata> {
40+
const blog = await getBlogFromParams({ params });
41+
42+
if (!blog) {
43+
return {};
44+
}
45+
46+
return {
47+
title: blog.title,
48+
description: blog.description,
49+
openGraph: {
50+
title: blog.title,
51+
description: blog.description,
52+
type: "article",
53+
url: absoluteUrl(blog.slug),
54+
images: [
55+
{
56+
url: siteConfig.ogImage,
57+
width: 1200,
58+
height: 630,
59+
alt: siteConfig.name,
60+
},
61+
],
62+
},
63+
twitter: {
64+
card: "summary_large_image",
65+
title: blog.title,
66+
description: blog.description,
67+
images: [siteConfig.ogImage],
68+
creator: blog.author ? `@${blog.author}` : "@AnimataDesign",
69+
},
70+
};
71+
}
72+
73+
export async function generateStaticParams(): Promise<BlogPageProps["params"][]> {
74+
return allBlogs.map((blog) => ({
75+
slug: blog.slugAsParams.split("/"),
76+
}));
77+
}
78+
79+
export default async function BlogPage({ params }: BlogPageProps) {
80+
const slug = params.slug?.join("/") || "";
81+
82+
// // Check if the slug is empty (i.e., the user is at /blog/)
83+
// if (slug === "") {
84+
// // Redirect to the first blog post
85+
// const firstBlogPost = blogSidebarNav[0]?.items[0]?.href; // Get the first blog post href
86+
// if (firstBlogPost) {
87+
// // Use the Next.js redirect method
88+
// redirect(firstBlogPost);
89+
// }
90+
// }
91+
92+
const isIndexPage = slug === "";
93+
94+
const blog = await getBlogFromParams({ params });
95+
96+
if (!blog) {
97+
notFound();
98+
}
99+
100+
const toc = await getTableOfContents(blog.body.raw);
101+
102+
return (
103+
<main className="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_150px]">
104+
<div className="mx-auto w-full min-w-0">
105+
<div className="mb-4 flex items-center space-x-1 text-sm text-muted-foreground">
106+
<div className="overflow-hidden text-ellipsis whitespace-nowrap">Blog</div>
107+
<ChevronRightIcon className="h-4 w-4" />
108+
{isIndexPage ? (
109+
<div className="overflow-hidden text-ellipsis whitespace-nowrap">Welcome</div>
110+
) : (
111+
<NavMenu baseRoute="blog" sideBarNavItems={blogSidebarNav} value={blog.slugAsParams} />
112+
)}
113+
</div>
114+
<div className="space-y-2">
115+
<h1 className={cn("scroll-m-20 text-4xl font-bold tracking-tight")}>{blog.title}</h1>
116+
{blog.description && (
117+
<p className="text-lg text-muted-foreground">
118+
<Balancer>{blog.description}</Balancer>
119+
</p>
120+
)}
121+
<div
122+
className={cn("flex items-center space-x-2 text-sm text-muted-foreground", {
123+
invisible: !blog.labels?.length,
124+
})}
125+
>
126+
{blog.labels?.map((label) => {
127+
return (
128+
<span key={label} className={cn(badgeVariants({ variant: "secondary" }), "gap-1")}>
129+
{label}
130+
</span>
131+
);
132+
})}
133+
</div>
134+
</div>
135+
{blog.links ? (
136+
<div className="flex items-center space-x-2 pt-4">
137+
{blog.links?.doc && (
138+
<Link
139+
href={blog.links.doc}
140+
target="_blank"
141+
rel="noreferrer"
142+
className={cn(badgeVariants({ variant: "secondary" }), "gap-1")}
143+
>
144+
Docs
145+
<ExternalLinkIcon className="h-3 w-3" />
146+
</Link>
147+
)}
148+
{blog.links?.api && (
149+
<Link
150+
href={blog.links.api}
151+
target="_blank"
152+
rel="noreferrer"
153+
className={cn(badgeVariants({ variant: "secondary" }), "gap-1")}
154+
>
155+
API Reference
156+
<ExternalLinkIcon className="h-3 w-3" />
157+
</Link>
158+
)}
159+
</div>
160+
) : null}
161+
<div className="pb-12">
162+
<Mdx code={blog.body.code} />
163+
164+
<div className="my-3 text-right">
165+
<Link
166+
href={`https://github.com/codse/animata/edit/main/content/docs/${blog.slugAsParams}.mdx`}
167+
target="_blank"
168+
rel="noreferrer"
169+
className="text-sm text-secondary-foreground underline"
170+
>
171+
Edit this page on GitHub
172+
</Link>
173+
</div>
174+
</div>
175+
<DocsPager doc={blog as unknown as Doc} />
176+
</div>
177+
{blog.toc && (
178+
<div className="hidden text-sm xl:block">
179+
<div className="sticky top-16 -mt-10 pt-4">
180+
<ScrollArea className="pb-10">
181+
<div className="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] py-12">
182+
<DashboardTableOfContents toc={toc} />
183+
</div>
184+
</ScrollArea>
185+
</div>
186+
</div>
187+
)}
188+
</main>
189+
);
190+
}

app/blog/layout.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { DocsSidebarNav } from "@/components/sidebar-nav";
2+
import { ScrollArea } from "@/components/ui/scroll-area";
3+
import { blogSidebarNav } from "@/config/blog";
4+
5+
interface BlogLayoutProps {
6+
children: React.ReactNode;
7+
}
8+
9+
export default function BlogLayout({ children }: BlogLayoutProps) {
10+
return (
11+
<div className="border-b border-border">
12+
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-4 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-6">
13+
<aside className="fixed top-14 z-30 -ml-2 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
14+
<ScrollArea className="h-full py-6 pr-6 lg:py-8">
15+
<DocsSidebarNav items={blogSidebarNav} />
16+
</ScrollArea>
17+
</aside>
18+
{children}
19+
</div>
20+
</div>
21+
);
22+
}

app/docs/[[...slug]]/nav-menu.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,23 @@ import {
1313
SelectTrigger,
1414
SelectValue,
1515
} from "@/components/ui/select";
16-
import { docsConfig } from "@/config/docs";
16+
import { SidebarNavItem } from "@/types";
1717

18-
export default function NavMenu({ value }: { value: string }) {
18+
export default function NavMenu({
19+
value,
20+
sideBarNavItems,
21+
baseRoute,
22+
}: {
23+
value: string;
24+
sideBarNavItems: SidebarNavItem[];
25+
baseRoute: "docs" | "blog";
26+
}) {
1927
const router = useRouter();
2028
const [navigating, setNavigating] = useState(false);
2129
return (
2230
<>
2331
<Select
24-
defaultValue={`/docs${value ? `/${value}` : ""}`}
32+
defaultValue={`/${baseRoute}${value ? `/${value}` : ""}`}
2533
onValueChange={(value) => {
2634
if (value) {
2735
setNavigating(true);
@@ -33,7 +41,7 @@ export default function NavMenu({ value }: { value: string }) {
3341
<SelectValue placeholder="Change page" />
3442
</SelectTrigger>
3543
<SelectContent>
36-
{docsConfig.sidebarNav.map((item, index) => (
44+
{sideBarNavItems.map((item, index) => (
3745
<SelectGroup key={index}>
3846
<SelectLabel className="font-medium">{item.title}</SelectLabel>
3947
{item?.items?.length &&

app/docs/[[...slug]]/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DocsPager } from "@/components/pager";
1010
import { DashboardTableOfContents } from "@/components/toc";
1111
import { badgeVariants } from "@/components/ui/badge";
1212
import { ScrollArea } from "@/components/ui/scroll-area";
13+
import { docsConfig } from "@/config/docs";
1314
import { siteConfig } from "@/config/site";
1415
import { getTableOfContents } from "@/lib/toc";
1516
import { absoluteUrl, cn } from "@/lib/utils";
@@ -90,7 +91,11 @@ export default async function DocPage({ params }: DocPageProps) {
9091
<div className="mb-4 flex items-center space-x-1 text-sm text-muted-foreground">
9192
<div className="overflow-hidden text-ellipsis whitespace-nowrap">Docs</div>
9293
<ChevronRightIcon className="h-4 w-4" />
93-
<NavMenu value={doc.slugAsParams} />
94+
<NavMenu
95+
baseRoute="docs"
96+
sideBarNavItems={docsConfig.sidebarNav}
97+
value={doc.slugAsParams}
98+
/>
9499
</div>
95100
<div className="space-y-2">
96101
<h1 className={cn("scroll-m-20 text-4xl font-bold tracking-tight")}>{doc.title}</h1>

config/blog.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { allBlogs } from "contentlayer/generated";
2+
3+
import { SidebarNavItem } from "@/types";
4+
5+
interface BlogPost extends SidebarNavItem {
6+
date: Date;
7+
}
8+
9+
const sortAlphabetically = (a: BlogPost, b: BlogPost) => {
10+
if (a.sortId && b.sortId) {
11+
return a.sortId.localeCompare(b.sortId);
12+
} else if (a.sortId) {
13+
return -1;
14+
} else if (b.sortId) {
15+
return 1;
16+
} else {
17+
return new Date(a.date).getTime() - new Date(b.date).getTime();
18+
}
19+
};
20+
21+
const createLinks = (category: string): BlogPost[] => {
22+
return allBlogs
23+
.filter((doc) => doc.published && doc.slug !== "/blog")
24+
.map((doc) => ({
25+
// Make sure the index page is the first item
26+
title: doc.title,
27+
sortId: doc.slug === `/blog/${category}` ? "000" : doc.title,
28+
href: doc.slug,
29+
items: [],
30+
date: new Date(doc.date ?? Date.now()),
31+
}))
32+
.sort(sortAlphabetically);
33+
};
34+
35+
export const blogSidebarNav = [
36+
{
37+
title: "Recent Posts",
38+
href: createLinks("blog")[0].href,
39+
items: createLinks("blog"),
40+
},
41+
];

config/docs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ export const docsConfig: DocsConfig = {
195195
title: "Components",
196196
href: sidebarNav[2].items?.[0]?.href ?? sidebarNav[2]?.href,
197197
},
198+
{
199+
title: "Blog",
200+
href: "/blog",
201+
},
198202
],
199203
sidebarNav,
200204
};

content/blog/hacktoberfest-2024.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
title: Hacktoberfest 2024
3+
date: 2024-09-20
4+
description: Animata celebrates Hacktoberfest 2024!
5+
---
6+
7+
Welcome to Animata’s Hacktoberfest 2024! 🎉 We're excited to have you contribute to our open-source project—**Animata**: a collection of customizable animation components for React & Tailwind projects.
8+
9+
In this post, we will provide the guidelines and information for contributing to Hacktoberfest. Whether you're working on a new animation component, fixing bugs, or improving documentation, this will be the central hub for all details related to your contributions.
10+
11+
### How to Get Involved:
12+
13+
1. **Explore Our Repository**: Check out the [official docs](https://animata.design/docs) and get familiar with our components.
14+
2. **Choose an Issue**: Use our [GitHub Project Board](https://github.com/orgs/codse/projects/37/views/0) or look for issues labeled `hacktoberfest`. Feel free to pick one that interests you.
15+
3. **Discuss**: Comment on the issue you're interested in and let us know you're working on it. If you have questions, we're here to help!
16+
4. **Submit a PR**: Once you’ve completed your task, submit a pull request (PR) referencing the issue number.
17+
5. **Follow Contribution Guidelines**: Before you submit your PR, make sure you’ve read and followed our [Contributing Guidelines](https://www.animata.design/docs/contributing).
18+
19+
### What Can You Contribute?
20+
21+
- Add new animation components to the library.
22+
- Refactor or improve existing components.
23+
- Fix/report bugs, or add new features.
24+
- Improve documentation for ease of use.
25+
- Enhance testing to ensure cross-browser compatibility.
26+
27+
### Tracking Progress
28+
29+
You can track the progress and pick tasks by visiting our [GitHub Project Board](https://github.com/orgs/codse/projects/37/views/0). The board is continuously updated with new issues and tasks for Hacktoberfest.
30+
31+
### How to Submit Your Work:
32+
33+
- Ensure your PR follows our contribution guidelines.
34+
- Link the issue number in your pull request description (e.g., "Fixes #issue-number").
35+
- Add relevant labels (e.g., `hacktoberfest`, `enhancement`, `bug-fix`).
36+
37+
---
38+
39+
### Contribution Rewards
40+
41+
We’re offering a **$50 reward** to the **top contributor** who adds the most impactful contributions, whether it's adding new components, resolving complex bugs, or improving documentation. Contributions will be judged based on:
42+
43+
- Quality of the work.
44+
- Value added to the project.
45+
- Complexity of the solution.
46+
47+
Thank you for being part of Animata's Hacktoberfest 2024! We’re looking forward to your contributions and making Animata better with your help. Happy hacking! 🎉

0 commit comments

Comments
 (0)