Skip to content

Commit 93d9e59

Browse files
committed
refactor: migrate tutorials page to SSR
1 parent 1f65410 commit 93d9e59

File tree

4 files changed

+154
-117
lines changed

4 files changed

+154
-117
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use client"
2+
3+
import React, { useState } from "react"
4+
import { FaGithub } from "react-icons/fa"
5+
6+
import Translation from "@/components/Translation"
7+
import { Button } from "@/components/ui/buttons/Button"
8+
import { ButtonLink } from "@/components/ui/buttons/Button"
9+
import Modal from "@/components/ui/dialog-modal"
10+
import { Flex } from "@/components/ui/flex"
11+
12+
import { trackCustomEvent } from "@/lib/utils/matomo"
13+
14+
import { useBreakpointValue } from "@/hooks/useBreakpointValue"
15+
16+
const TutorialSubmitModal = ({
17+
dir,
18+
}: Pick<React.HTMLAttributes<React.JSX.Element>, "dir">) => {
19+
const [isModalOpen, setModalOpen] = useState(false)
20+
21+
const modalSize = useBreakpointValue({ base: "xl", md: "md" } as const)
22+
23+
return (
24+
<>
25+
<Modal
26+
open={isModalOpen}
27+
onOpenChange={(open) => setModalOpen(open)}
28+
size={modalSize}
29+
contentProps={{ dir }}
30+
title={
31+
<Translation id="page-developers-tutorials:page-tutorial-submit-btn" />
32+
}
33+
>
34+
<p className="mb-6">
35+
<Translation id="page-developers-tutorials:page-tutorial-listing-policy-intro" />
36+
</p>
37+
<Flex className="flex-col gap-2 md:flex-row">
38+
<Flex className="w-full flex-col justify-between rounded-sm border border-border p-4">
39+
<b>
40+
<Translation id="page-developers-tutorials:page-tutorial-create-an-issue" />
41+
</b>
42+
<p className="mb-6">
43+
<Translation id="page-developers-tutorials:page-tutorial-create-an-issue-desc" />
44+
</p>
45+
<ButtonLink
46+
variant="outline"
47+
href="https://github.com/ethereum/ethereum-org-website/issues/new?assignees=&labels=Type%3A+Feature&template=suggest_tutorial.yaml&title="
48+
>
49+
<FaGithub />
50+
<Translation id="page-developers-tutorials:page-tutorial-raise-issue-btn" />
51+
</ButtonLink>
52+
</Flex>
53+
</Flex>
54+
</Modal>
55+
56+
<Button
57+
className="px-3 py-2 text-body"
58+
variant="outline"
59+
onClick={() => {
60+
setModalOpen(true)
61+
trackCustomEvent({
62+
eventCategory: "tutorials tags",
63+
eventAction: "click",
64+
eventName: "submit",
65+
})
66+
}}
67+
>
68+
<Translation id="page-developers-tutorials:page-tutorial-submit-btn" />
69+
</Button>
70+
</>
71+
)
72+
}
73+
74+
TutorialSubmitModal.displayName = "TutorialSubmitModal"
75+
76+
export default TutorialSubmitModal

app/[locale]/developers/tutorials/_components/tutorials.tsx

Lines changed: 12 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,14 @@ import React, {
99
useState,
1010
} from "react"
1111
import { useLocale } from "next-intl"
12-
import { FaGithub } from "react-icons/fa"
1312

1413
import { ITutorial, Lang } from "@/lib/types"
1514

1615
import Emoji from "@/components/Emoji"
17-
import FeedbackCard from "@/components/FeedbackCard"
18-
import MainArticle from "@/components/MainArticle"
1916
import Translation from "@/components/Translation"
2017
import { getSkillTranslationId } from "@/components/TutorialMetadata"
2118
import TutorialTags from "@/components/TutorialTags"
22-
import { Button, ButtonLink } from "@/components/ui/buttons/Button"
23-
import Modal from "@/components/ui/dialog-modal"
19+
import { Button } from "@/components/ui/buttons/Button"
2420
import { Flex, FlexProps } from "@/components/ui/flex"
2521
import { Tag, TagButton } from "@/components/ui/tag"
2622

@@ -36,12 +32,6 @@ import externalTutorials from "@/data/externalTutorials.json"
3632

3733
import { DEFAULT_LOCALE } from "@/lib/constants"
3834

39-
import { useBreakpointValue } from "@/hooks/useBreakpointValue"
40-
41-
type LinkFlexProps = FlexProps & {
42-
href: string
43-
}
44-
4535
const FilterTag = forwardRef<
4636
HTMLButtonElement,
4737
{ isActive: boolean; name: string } & ButtonHTMLAttributes<HTMLButtonElement>
@@ -66,6 +56,10 @@ const Text = ({ className, ...props }: HTMLAttributes<HTMLHeadElement>) => (
6656
<p className={cn("mb-6", className)} {...props} />
6757
)
6858

59+
type LinkFlexProps = FlexProps & {
60+
href: string
61+
}
62+
6963
const LinkFlex = ({ href, children, ...props }: LinkFlexProps) => {
7064
return (
7165
<Flex asChild {...props}>
@@ -85,15 +79,11 @@ const published = (locale: string, published: string) => {
8579
) : null
8680
}
8781

88-
type TutorialPageProps = {
82+
type TutorialsListProps = {
8983
internalTutorials: ITutorial[]
90-
contentNotTranslated: boolean
9184
}
9285

93-
const TutorialPage = ({
94-
internalTutorials,
95-
contentNotTranslated,
96-
}: TutorialPageProps) => {
86+
const TutorialsList = ({ internalTutorials }: TutorialsListProps) => {
9787
const locale = useLocale()
9888
const effectiveLocale = internalTutorials.length > 0 ? locale : DEFAULT_LOCALE
9989
const filteredTutorialsByLang = useMemo(
@@ -111,7 +101,6 @@ const TutorialPage = ({
111101
[filteredTutorialsByLang]
112102
)
113103

114-
const [isModalOpen, setModalOpen] = useState(false)
115104
const [filteredTutorials, setFilteredTutorials] = useState(
116105
filteredTutorialsByLang
117106
)
@@ -152,66 +141,8 @@ const TutorialPage = ({
152141
setSelectedTags([...tempSelectedTags])
153142
}
154143

155-
const dir = contentNotTranslated ? "ltr" : "unset"
156-
157-
const modalSize = useBreakpointValue({ base: "xl", md: "md" } as const)
158144
return (
159-
<MainArticle
160-
className={`mx-auto my-0 mt-16 flex w-full flex-col items-center ${dir}`}
161-
>
162-
<h1 className="no-italic mb-4 text-center font-monospace text-[2rem] font-semibold uppercase leading-[1.4] max-sm:mx-4 max-sm:mt-4 sm:mb-[1.625rem]">
163-
<Translation id="page-developers-tutorials:page-tutorial-title" />
164-
</h1>
165-
<Text className="mb-4 text-center leading-xs text-body-medium">
166-
<Translation id="page-developers-tutorials:page-tutorial-subtitle" />
167-
</Text>
168-
169-
<Modal
170-
open={isModalOpen}
171-
onOpenChange={(open) => setModalOpen(open)}
172-
size={modalSize}
173-
contentProps={{ dir }}
174-
title={
175-
<Translation id="page-developers-tutorials:page-tutorial-submit-btn" />
176-
}
177-
>
178-
<Text>
179-
<Translation id="page-developers-tutorials:page-tutorial-listing-policy-intro" />
180-
</Text>
181-
<Flex className="flex-col gap-2 md:flex-row">
182-
<Flex className="w-full flex-col justify-between rounded-sm border border-border p-4">
183-
<b>
184-
<Translation id="page-developers-tutorials:page-tutorial-create-an-issue" />
185-
</b>
186-
<Text>
187-
<Translation id="page-developers-tutorials:page-tutorial-create-an-issue-desc" />
188-
</Text>
189-
<ButtonLink
190-
variant="outline"
191-
href="https://github.com/ethereum/ethereum-org-website/issues/new?assignees=&labels=Type%3A+Feature&template=suggest_tutorial.yaml&title="
192-
>
193-
<FaGithub />
194-
<Translation id="page-developers-tutorials:page-tutorial-raise-issue-btn" />
195-
</ButtonLink>
196-
</Flex>
197-
</Flex>
198-
</Modal>
199-
200-
<Button
201-
className="px-3 py-2 text-body"
202-
variant="outline"
203-
onClick={() => {
204-
setModalOpen(true)
205-
trackCustomEvent({
206-
eventCategory: "tutorials tags",
207-
eventAction: "click",
208-
eventName: "submit",
209-
})
210-
}}
211-
>
212-
<Translation id="page-developers-tutorials:page-tutorial-submit-btn" />
213-
</Button>
214-
145+
<>
215146
<div className="my-8 w-full shadow-table-box md:w-2/3">
216147
<Flex className="m-8 flex-col justify-center border-b border-border px-0 pb-4 pt-4 md:pb-8">
217148
<Flex className="mb-4 max-w-full flex-wrap items-center gap-2">
@@ -312,9 +243,10 @@ const TutorialPage = ({
312243
)
313244
})}
314245
</div>
315-
<FeedbackCard />
316-
</MainArticle>
246+
</>
317247
)
318248
}
319249

320-
export default TutorialPage
250+
TutorialsList.displayName = "TutorialsList"
251+
252+
export default TutorialsList

app/[locale]/developers/tutorials/page.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,28 @@ import {
77

88
import { Lang } from "@/lib/types"
99

10+
import FeedbackCard from "@/components/FeedbackCard"
1011
import I18nProvider from "@/components/I18nProvider"
12+
import MainArticle from "@/components/MainArticle"
1113

1214
import { existsNamespace } from "@/lib/utils/existsNamespace"
1315
import { getTutorialsData } from "@/lib/utils/md"
1416
import { getMetadata } from "@/lib/utils/metadata"
1517
import { getRequiredNamespacesForPage } from "@/lib/utils/translations"
1618

17-
import Tutorials from "./_components/tutorials"
19+
import TutorialSubmitModal from "./_components/modal"
20+
import TutorialsList from "./_components/tutorials"
1821

1922
const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
2023
const { locale } = await params
2124

2225
setRequestLocale(locale)
2326

27+
const t = await getTranslations({
28+
locale,
29+
namespace: "page-developers-tutorials",
30+
})
31+
2432
// Get i18n messages
2533
const allMessages = await getMessages({ locale })
2634
const requiredNamespaces = getRequiredNamespacesForPage(
@@ -29,13 +37,29 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
2937
const messages = pick(allMessages, requiredNamespaces)
3038

3139
const contentNotTranslated = !existsNamespace(locale!, requiredNamespaces[2])
40+
const dir = contentNotTranslated ? "ltr" : "unset"
41+
42+
const internalTutorials = await getTutorialsData(locale)
3243

3344
return (
3445
<I18nProvider locale={locale} messages={messages}>
35-
<Tutorials
36-
internalTutorials={getTutorialsData(locale)}
37-
contentNotTranslated={contentNotTranslated}
38-
/>
46+
<MainArticle
47+
className="mx-auto my-0 mt-16 flex w-full flex-col items-center"
48+
dir={dir}
49+
>
50+
<h1 className="no-italic mb-4 text-center font-monospace text-[2rem] font-semibold uppercase leading-[1.4] max-sm:mx-4 max-sm:mt-4 sm:mb-[1.625rem]">
51+
{t("page-tutorial-title")}
52+
</h1>
53+
<p className="mb-4 text-center leading-xs text-body-medium">
54+
{t("page-tutorial-subtitle")}
55+
</p>
56+
57+
<TutorialSubmitModal dir={dir} />
58+
59+
<TutorialsList internalTutorials={internalTutorials} />
60+
61+
<FeedbackCard />
62+
</MainArticle>
3963
</I18nProvider>
4064
)
4165
}

src/lib/utils/md.ts

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from "fs"
21
import fsp from "fs/promises"
32
import { extname, join } from "path"
43

@@ -59,44 +58,50 @@ export const getPostSlugs = async (dir: string, filterRegex?: RegExp) => {
5958
return files
6059
}
6160

62-
export const getTutorialsData = (locale: string): ITutorial[] => {
61+
export const getTutorialsData = async (
62+
locale: string
63+
): Promise<ITutorial[]> => {
6364
const contentRoot = getContentRoot()
6465
const fullPath = join(
6566
contentRoot,
6667
locale !== "en" ? `translations/${locale!}` : "",
6768
"developers/tutorials"
6869
)
69-
let tutorialData: ITutorial[] = []
70-
71-
if (fs.existsSync(fullPath)) {
72-
const languageTutorialFiles = fs.readdirSync(fullPath)
73-
74-
tutorialData = languageTutorialFiles.map((dir) => {
75-
const filePath = join(
76-
contentRoot,
77-
locale !== "en" ? `translations/${locale!}` : "",
78-
"developers/tutorials",
79-
dir,
80-
"index.md"
81-
)
82-
const fileContents = fs.readFileSync(filePath, "utf8")
83-
const { data, content } = matter(fileContents)
84-
const frontmatter = data as Frontmatter
85-
86-
return {
87-
href: join(`/${locale}/developers/tutorials`, dir),
88-
title: frontmatter.title,
89-
description: frontmatter.description,
90-
author: frontmatter.author || "",
91-
tags: frontmatter.tags,
92-
skill: frontmatter.skill as Skill,
93-
timeToRead: Math.round(readingTime(content).minutes),
94-
published: dateToString(frontmatter.published),
95-
lang: frontmatter.lang,
96-
isExternal: false,
97-
}
98-
})
70+
const tutorialData: ITutorial[] = []
71+
72+
const stats = await fsp.stat(fullPath)
73+
if (!stats.isDirectory()) {
74+
console.warn(`Tutorials directory not found for locale: ${locale}`)
75+
return tutorialData // Return empty if the directory does not exist
9976
}
10077

78+
const languageTutorialFiles = await fsp.readdir(fullPath)
79+
80+
languageTutorialFiles.forEach(async (dir) => {
81+
const filePath = join(
82+
contentRoot,
83+
locale !== "en" ? `translations/${locale!}` : "",
84+
"developers/tutorials",
85+
dir,
86+
"index.md"
87+
)
88+
const fileContents = await fsp.readFile(filePath, "utf8")
89+
const { data, content } = matter(fileContents)
90+
const frontmatter = data as Frontmatter
91+
92+
tutorialData.push({
93+
href: join(`/${locale}/developers/tutorials`, dir),
94+
title: frontmatter.title,
95+
description: frontmatter.description,
96+
author: frontmatter.author || "",
97+
tags: frontmatter.tags,
98+
skill: frontmatter.skill as Skill,
99+
timeToRead: Math.round(readingTime(content).minutes),
100+
published: dateToString(frontmatter.published),
101+
lang: frontmatter.lang,
102+
isExternal: false,
103+
})
104+
})
105+
101106
return tutorialData
102107
}

0 commit comments

Comments
 (0)