11import { createElement } from "react" ;
2- import type { ComponentPropsWithoutRef } from "react" ;
2+ import type { ComponentPropsWithoutRef , ReactNode } from "react" ;
33import { notFound } from "next/navigation" ;
44import { ChallengeHeader } from "./_components/ChallengeHeader" ;
5+ import { ChallengeSidebar } from "./_components/ChallengeSidebar" ;
56import { ConnectAndRegisterBanner } from "./_components/ConnectAndRegisterBanner" ;
67import { SubmitChallengeButton } from "./_components/SubmitChallengeButton" ;
78import { MDXRemote } from "next-mdx-remote/rsc" ;
@@ -15,7 +16,7 @@ import {
1516 getCountOfCompletedChallenge ,
1617} from "~~/services/database/repositories/challenges" ;
1718import { fetchGithubChallengeReadme , parseGithubUrl , splitChallengeReadme } from "~~/services/github" ;
18- import { CHALLENGE_METADATA } from "~~/utils/challenges" ;
19+ import { CHALLENGE_METADATA , extractHeadings , generateHeadingId } from "~~/utils/challenges" ;
1920import { getMetadata } from "~~/utils/scaffold-eth/getMetadata" ;
2021
2122export async function generateStaticParams ( ) {
@@ -58,90 +59,107 @@ export default async function ChallengePage(props: { params: Promise<{ challenge
5859 const { headerImageMdx, restMdx } = splitChallengeReadme ( challengeReadme ) ;
5960 const { owner, repo, branch } = parseGithubUrl ( challenge . github ) ;
6061
62+ // Extract headings for the sidebar navigation
63+ const headings = extractHeadings ( restMdx ) ;
64+
65+ // Custom h2 component that adds IDs for anchor navigation
66+ const createH2WithId = ( { children, ...props } : { children ?: ReactNode } ) => {
67+ const text = String ( children ) ;
68+ const id = generateHeadingId ( text ) ;
69+ return createElement ( "h2" , { ...props , id, style : { scrollMarginTop : "80px" } } , children ) ;
70+ } ;
71+
6172 return (
62- < div className = "flex flex-col items-center py-8 px-5 xl:p-12 relative max-w-[100vw]" >
63- { challengeReadme ? (
64- < >
65- < div className = "prose dark:prose-invert max-w-fit break-words lg:max-w-[850px]" >
66- < MDXRemote
67- source = { headerImageMdx }
68- options = { {
69- mdxOptions : {
70- rehypePlugins : [ rehypeRaw ] ,
71- remarkPlugins : [ remarkGfm ] ,
72- format : "md" ,
73- } ,
74- } }
75- />
76- </ div >
77- < ChallengeHeader
78- skills = { staticMetadata ?. skills }
79- skillLevel = { staticMetadata ?. skillLevel }
80- timeToComplete = { staticMetadata ?. timeToComplete }
81- helpfulLinks = { staticMetadata ?. helpfulLinks }
82- completedByCount = { countOfCompletedChallenge }
83- />
84- < div className = "prose dark:prose-invert max-w-fit break-words lg:max-w-[850px]" >
85- < MDXRemote
86- source = { restMdx }
87- components = { {
88- a : ( props : ComponentPropsWithoutRef < "a" > ) =>
89- createElement ( "a" , { ...props , target : "_blank" , rel : "noopener" } ) ,
90- } }
91- options = { {
92- mdxOptions : {
93- rehypePlugins : [ rehypeRaw ] ,
94- remarkPlugins : [ remarkGfm ] ,
95- format : "md" ,
96- } ,
97- } }
98- />
99- </ div >
73+ < div className = "flex relative max-w-[100vw]" >
74+ { /* Sidebar Navigation */ }
75+ < ChallengeSidebar headings = { headings } />
10076
101- < a
102- href = { `https://github.com/${ owner } /${ repo } /tree/${ branch } ` }
103- className = "block mt-2"
104- target = "_blank"
105- rel = "noopener noreferrer"
106- >
107- < button className = "btn btn-outline btn-sm sm:btn-md" >
108- < span className = "text-xs sm:text-sm" > View on GitHub</ span >
109- < ArrowTopRightOnSquareIcon className = "w-3 h-3 sm:w-4 sm:h-4" />
110- </ button >
111- </ a >
112- { guides && guides . length > 0 && (
113- < div className = "max-w-[850px] w-full mx-auto" >
114- < div className = "mt-16 mb-4 font-semibold text-left" > Related guides</ div >
115- < div className = "grid grid-cols-1 md:grid-cols-2 gap-4 mt-2 mb-2" >
116- { guides . map ( guide => (
117- < div key = { guide . url } className = "p-4 border rounded bg-base-300" >
118- < a href = { guide . url } className = "text-primary underline font-semibold" >
119- { guide . title }
120- </ a >
121- </ div >
122- ) ) }
123- </ div >
77+ { /* Main Content */ }
78+ < div className = "flex-1 flex flex-col items-center py-8 px-5 xl:p-12" >
79+ { challengeReadme ? (
80+ < >
81+ < div className = "prose dark:prose-invert max-w-fit break-words lg:max-w-[850px]" >
82+ < MDXRemote
83+ source = { headerImageMdx }
84+ options = { {
85+ mdxOptions : {
86+ rehypePlugins : [ rehypeRaw ] ,
87+ remarkPlugins : [ remarkGfm ] ,
88+ format : "md" ,
89+ } ,
90+ } }
91+ />
92+ </ div >
93+ < ChallengeHeader
94+ skills = { staticMetadata ?. skills }
95+ skillLevel = { staticMetadata ?. skillLevel }
96+ timeToComplete = { staticMetadata ?. timeToComplete }
97+ helpfulLinks = { staticMetadata ?. helpfulLinks }
98+ completedByCount = { countOfCompletedChallenge }
99+ />
100+ < div className = "prose dark:prose-invert max-w-fit break-words lg:max-w-[850px]" >
101+ < MDXRemote
102+ source = { restMdx }
103+ components = { {
104+ a : ( props : ComponentPropsWithoutRef < "a" > ) =>
105+ createElement ( "a" , { ...props , target : "_blank" , rel : "noopener" } ) ,
106+ h2 : createH2WithId ,
107+ } }
108+ options = { {
109+ mdxOptions : {
110+ rehypePlugins : [ rehypeRaw ] ,
111+ remarkPlugins : [ remarkGfm ] ,
112+ format : "md" ,
113+ } ,
114+ } }
115+ />
124116 </ div >
125- ) }
126- </ >
127- ) : (
128- < div > Failed to load challenge content</ div >
129- ) }
130- { challenge . autograding && (
131- < >
132- < ConnectAndRegisterBanner />
133- < SubmitChallengeButton challengeId = { challenge . id } />
134- </ >
135- ) }
136- { challenge . externalLink && (
137- < div className = "fixed bottom-8 inset-x-0 mx-auto w-fit" >
138- < button className = "btn btn-sm sm:btn-md btn-primary text-secondary px-3 sm:px-4 mt-2 text-xs sm:text-sm" >
139- < a href = { challenge . externalLink . link } target = "_blank" rel = "noopener noreferrer" >
140- { challenge . externalLink . claim }
117+
118+ < a
119+ href = { `https://github.com/${ owner } /${ repo } /tree/${ branch } ` }
120+ className = "block mt-2"
121+ target = "_blank"
122+ rel = "noopener noreferrer"
123+ >
124+ < button className = "btn btn-outline btn-sm sm:btn-md" >
125+ < span className = "text-xs sm:text-sm" > View on GitHub</ span >
126+ < ArrowTopRightOnSquareIcon className = "w-3 h-3 sm:w-4 sm:h-4" />
127+ </ button >
141128 </ a >
142- </ button >
143- </ div >
144- ) }
129+ { guides && guides . length > 0 && (
130+ < div className = "max-w-[850px] w-full mx-auto" >
131+ < div className = "mt-16 mb-4 font-semibold text-left" > Related guides</ div >
132+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-4 mt-2 mb-2" >
133+ { guides . map ( guide => (
134+ < div key = { guide . url } className = "p-4 border rounded bg-base-300" >
135+ < a href = { guide . url } className = "text-primary underline font-semibold" >
136+ { guide . title }
137+ </ a >
138+ </ div >
139+ ) ) }
140+ </ div >
141+ </ div >
142+ ) }
143+ </ >
144+ ) : (
145+ < div > Failed to load challenge content</ div >
146+ ) }
147+ { challenge . autograding && (
148+ < >
149+ < ConnectAndRegisterBanner />
150+ < SubmitChallengeButton challengeId = { challenge . id } />
151+ </ >
152+ ) }
153+ { challenge . externalLink && (
154+ < div className = "fixed bottom-8 inset-x-0 mx-auto w-fit" >
155+ < button className = "btn btn-sm sm:btn-md btn-primary text-secondary px-3 sm:px-4 mt-2 text-xs sm:text-sm" >
156+ < a href = { challenge . externalLink . link } target = "_blank" rel = "noopener noreferrer" >
157+ { challenge . externalLink . claim }
158+ </ a >
159+ </ button >
160+ </ div >
161+ ) }
162+ </ div >
145163 </ div >
146164 ) ;
147165}
0 commit comments