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 + + + blog-image + + {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} + {"explorer-blog-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 />