diff --git a/explorer-nextjs/public/images/placeholder.webp b/explorer-nextjs/public/images/placeholder.webp
new file mode 100644
index 0000000000..2c5e4325ee
Binary files /dev/null and b/explorer-nextjs/public/images/placeholder.webp differ
diff --git a/explorer-nextjs/src/app/(pages)/onboarding/[slug]/page.tsx b/explorer-nextjs/src/app/(pages)/onboarding/[slug]/page.tsx
new file mode 100644
index 0000000000..c5d19791c4
--- /dev/null
+++ b/explorer-nextjs/src/app/(pages)/onboarding/[slug]/page.tsx
@@ -0,0 +1,142 @@
+import TableOfContents from "@/components/blogs/TableOfContents";
+import type BlogArticle from "@/components/blogs/types";
+import { Breadcrumbs } from "@/components/breadcrumbs/Breadcrumbs";
+import { ContentLayout } from "@/components/contentLayout/ContentLayout";
+import SectionHeading from "@/components/headings/SectionHeading";
+import { Link } from "@/components/muiLink";
+import { Wrapper } from "@/components/wrapper";
+import { Box, Stack, Typography } from "@mui/material";
+import Grid from "@mui/material/Grid2";
+import { format } from "date-fns";
+import Image from "next/image";
+
+export default async function BlogPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+
+ try {
+ const blogArticle: BlogArticle = await import(`@/data/${slug}.json`);
+ const breadcrumbItems = [
+ {
+ label: "Onboarding",
+ href: "/onboarding",
+ },
+ { label: blogArticle.title, isCurrentPage: true },
+ ];
+ return (
+
+
+
+
+
+
+
+
+
+
+ Author
+ {(blogArticle?.attributes?.blogAuthors?.length ?? 0) > 1
+ ? "s"
+ : ""}
+ :{" "}
+ {blogArticle?.attributes?.blogAuthors?.map(
+ (author: string) => (
+
+ {author}
+
+ ),
+ )}
+
+
+
+
+ {blogArticle.attributes.readingTime}{" "}
+ {blogArticle.attributes.readingTime > 1 ? "mins" : "min"}{" "}
+ read
+
+
+
+
+ {blogArticle.overview.content.map(({ text }) => (
+
+ {text}
+
+ ))}
+
+ {blogArticle.sections.map((section) => (
+
+
+ {section.text.map(({ text }) => (
+
+ {text}
+
+ ))}
+
+ ))}
+
+
+
+ ({
+ heading: section.heading,
+ id: section.id,
+ }))}
+ />
+
+
+
+
+ );
+ } catch (error) {
+ console.log(error);
+
+ return (
+
+
+
+
+ Oops! Looks like the page you’re looking for got mixed up in the
+ noise. Don’t worry, your privacy is intact. Let’s get you
+ back to the homepage.
+
+
+
+ );
+ }
+}
diff --git a/explorer-nextjs/src/app/(pages)/onboarding/page.tsx b/explorer-nextjs/src/app/(pages)/onboarding/page.tsx
index af76e54ed4..e0f977d7e0 100644
--- a/explorer-nextjs/src/app/(pages)/onboarding/page.tsx
+++ b/explorer-nextjs/src/app/(pages)/onboarding/page.tsx
@@ -1,16 +1,15 @@
-import { Wrapper } from "@/components/wrapper";
-import { Box, Typography } from "@mui/material";
+import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
+import { ContentLayout } from "@/components/contentLayout/ContentLayout";
+import SectionHeading from "@/components/headings/SectionHeading";
+import Grid from "@mui/material/Grid2";
export default function OnboardingPage() {
return (
-
-
-
-
- Onboarding page
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/explorer-nextjs/src/app/globals.css b/explorer-nextjs/src/app/globals.css
index 79be221e4f..9e6cea0114 100644
--- a/explorer-nextjs/src/app/globals.css
+++ b/explorer-nextjs/src/app/globals.css
@@ -19,7 +19,6 @@
html,
body {
max-width: 100vw;
- overflow-x: hidden;
}
a {
diff --git a/explorer-nextjs/src/app/page.tsx b/explorer-nextjs/src/app/page.tsx
index 55068c9856..ad3feff129 100644
--- a/explorer-nextjs/src/app/page.tsx
+++ b/explorer-nextjs/src/app/page.tsx
@@ -1,8 +1,7 @@
-import ExplorerHeroCard from "@/components/cards/ExplorerHeroCard";
+import BlogArticlesCards from "@/components/blogs/BlogArticleCards";
import CardSkeleton from "@/components/cards/Skeleton";
import { ContentLayout } from "@/components/contentLayout/ContentLayout";
import SectionHeading from "@/components/headings/SectionHeading";
-import Gateway from "@/components/icons/Gateway";
import { CurrentEpochCard } from "@/components/landingPageComponents/CurrentEpochCard";
import { NetworkStakeCard } from "@/components/landingPageComponents/NetworkStakeCard";
import { NoiseCard } from "@/components/landingPageComponents/NoiseCard";
@@ -14,7 +13,7 @@ import { Stack, Typography } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { Suspense } from "react";
-export default function Home() {
+export default async function Home() {
return (
@@ -61,26 +60,7 @@ export default function Home() {
-
- }
- link={"/onboarding"}
- sx={{ width: "100%" }}
- />
-
-
- }
- link={"/onboarding"}
- sx={{ width: "100%" }}
- />
-
+
);
diff --git a/explorer-nextjs/src/components/blogs/BlogArticleCards.tsx b/explorer-nextjs/src/components/blogs/BlogArticleCards.tsx
new file mode 100644
index 0000000000..15e76ef84b
--- /dev/null
+++ b/explorer-nextjs/src/components/blogs/BlogArticleCards.tsx
@@ -0,0 +1,60 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+import Grid from "@mui/material/Grid2";
+import ExplorerHeroCard from "../cards/ExplorerHeroCard";
+import type { BlogArticleWithLink } from "./types";
+
+// TODO: Articles should be sorted by date
+
+const BlogArticlesCards = async ({ limit }: { limit?: number }) => {
+ const blogsDir = path.join(process.cwd(), "/src/data");
+ const blogsDirFilenames = await fs.readdir(blogsDir);
+
+ // Read all blog articles from the data directory
+ const blogArticles: BlogArticleWithLink[] = await Promise.all(
+ blogsDirFilenames.map(async (filename) => {
+ const filePath = path.join(blogsDir, filename);
+ const fileContent = await fs.readFile(filePath, "utf-8");
+ const blogArticle = JSON.parse(fileContent);
+ return {
+ ...blogArticle,
+ link: `/onboarding/${filename.replace(".json", "")}`,
+ };
+ }),
+ );
+
+ const limitedBlogArticles = limit
+ ? blogArticles.slice(0, limit)
+ : blogArticles;
+
+ return limitedBlogArticles
+ .sort((a, b) => {
+ // sort by date
+ return (
+ new Date(a.attributes.date).getTime() -
+ new Date(b.attributes.date).getTime()
+ );
+ })
+ .map((blogArticle) => {
+ return (
+
+
+
+ );
+ });
+};
+
+export default BlogArticlesCards;
diff --git a/explorer-nextjs/src/components/blogs/TableOfContents.tsx b/explorer-nextjs/src/components/blogs/TableOfContents.tsx
new file mode 100644
index 0000000000..cc95cc9c3c
--- /dev/null
+++ b/explorer-nextjs/src/components/blogs/TableOfContents.tsx
@@ -0,0 +1,37 @@
+import { Card, CardContent, CardHeader, Typography } from "@mui/material";
+import { Link } from "../muiLink";
+
+const TableOfContents = ({
+ headings,
+}: {
+ headings: { id: string; heading: string }[];
+}) => {
+ return (
+
+
+
+ {headings.map((heading) => (
+
+
+ {heading.heading}
+
+
+ ))}
+
+
+ );
+};
+
+export default TableOfContents;
diff --git a/explorer-nextjs/src/components/blogs/types.ts b/explorer-nextjs/src/components/blogs/types.ts
new file mode 100644
index 0000000000..4d141e7f0e
--- /dev/null
+++ b/explorer-nextjs/src/components/blogs/types.ts
@@ -0,0 +1,28 @@
+type Content = { type: string; text: string };
+
+type BlogArticle = {
+ title: string;
+ label: string;
+ description: string;
+ image: string;
+ icon: string;
+ attributes: {
+ blogAuthors: string[];
+ date: Date;
+ readingTime: number;
+ };
+ overview: {
+ content: Content[];
+ };
+ sections: {
+ id: string;
+ heading: string;
+ text: Content[];
+ }[];
+};
+
+export type BlogArticleWithLink = BlogArticle & {
+ link: string;
+};
+
+export default BlogArticle;
diff --git a/explorer-nextjs/src/components/breadcrumbs/Breadcrumbs.tsx b/explorer-nextjs/src/components/breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 0000000000..4c5474d6e9
--- /dev/null
+++ b/explorer-nextjs/src/components/breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,70 @@
+import BreadcrumbsMUI from "@mui/material/Breadcrumbs";
+import Typography from "@mui/material/Typography";
+import Link from "next/link";
+
+interface BreadcrumbComponentProps {
+ items: BreadcrumbItemType[];
+}
+
+interface BreadcrumbItemType {
+ label: string;
+ href?: string;
+ isCurrentPage?: boolean;
+}
+
+export const Breadcrumbs = ({ items }: BreadcrumbComponentProps) => {
+ return (
+
+ /
+
+ }
+ aria-label="breadcrumb"
+ >
+ {items.map((item) => {
+ // Check if it's the current page
+ if (item.isCurrentPage) {
+ return (
+
+ {item.label}
+
+ );
+ }
+
+ // If it's not the current page, render a clickable link
+ return (
+
+
+ {item.label}
+
+
+ );
+ })}
+
+ );
+};
diff --git a/explorer-nextjs/src/components/cards/ExplorerHeroCard.tsx b/explorer-nextjs/src/components/cards/ExplorerHeroCard.tsx
index 0e97f033a7..16d6f47214 100644
--- a/explorer-nextjs/src/components/cards/ExplorerHeroCard.tsx
+++ b/explorer-nextjs/src/components/cards/ExplorerHeroCard.tsx
@@ -7,6 +7,7 @@ import {
type SxProps,
Typography,
} from "@mui/material";
+import Image from "next/image";
import { Link } from "../muiLink";
const cardStyles = {
@@ -27,14 +28,14 @@ const ExplorerHeroCard = ({
title,
label,
description,
- image,
+ icon,
link,
sx,
}: {
title: string;
label: string;
description: string;
- image: React.ReactNode;
+ icon: string;
link: string;
sx?: SxProps;
}) => {
@@ -53,7 +54,12 @@ const ExplorerHeroCard = ({
/>
- {image}
+
{title}
{description}
diff --git a/explorer-nextjs/src/components/input/Input.tsx b/explorer-nextjs/src/components/input/Input.tsx
index 6c4cede926..5924c66cfe 100644
--- a/explorer-nextjs/src/components/input/Input.tsx
+++ b/explorer-nextjs/src/components/input/Input.tsx
@@ -1,13 +1,16 @@
-import { TextField } from "@mui/material";
+import { type SxProps, TextField } from "@mui/material";
const Input = ({
placeholder,
fullWidth,
value,
+ rounded = false,
onChange,
}: {
placeholder?: string;
fullWidth?: boolean;
+ rounded?: boolean;
+ sx?: SxProps;
value: string;
onChange: (event: React.ChangeEvent) => void;
}) => {
@@ -17,6 +20,13 @@ const Input = ({
fullWidth={fullWidth}
value={value}
onChange={onChange}
+ slotProps={{
+ input: {
+ sx: {
+ borderRadius: rounded ? 10 : 2,
+ },
+ },
+ }}
/>
);
};
diff --git a/explorer-nextjs/src/components/search/NodeAndAddressSearch.tsx b/explorer-nextjs/src/components/search/NodeAndAddressSearch.tsx
index 0df65de5c5..28e4c250f3 100644
--- a/explorer-nextjs/src/components/search/NodeAndAddressSearch.tsx
+++ b/explorer-nextjs/src/components/search/NodeAndAddressSearch.tsx
@@ -73,6 +73,7 @@ const NodeAndAddressSearch = () => {
fullWidth
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
+ rounded
/>