From 47c236388b3f016a9210d865aa9efdde56929ae5 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Thu, 25 Jan 2024 09:59:39 +0100 Subject: [PATCH 01/28] wip --- .github/workflows/stage-build.yml | 10 + build/build-curriculum.ts | 4 + build/curriculum.ts | 276 +++++++++++++++++++ build/utils.ts | 33 ++- client/src/app.tsx | 9 + client/src/assets/icons/cur-ext-resource.svg | 12 + client/src/assets/icons/cur-mdn-resource.svg | 8 + client/src/assets/icons/cur-resources.svg | 12 + client/src/curriculum/module.scss | 219 +++++++++++++++ client/src/curriculum/module.tsx | 88 ++++++ client/src/document/organisms/toc/index.tsx | 4 +- client/src/placement-context.tsx | 2 +- client/src/ui/molecules/main-menu/index.tsx | 1 + cloud-function/src/app.ts | 2 +- deployer/src/deployer/search/__init__.py | 5 + libs/env/index.d.ts | 1 + libs/env/index.js | 2 + libs/types/curriculum.ts | 28 ++ libs/types/document.ts | 2 + libs/types/hydration.ts | 2 + package.json | 1 + server/index.ts | 25 ++ 22 files changed, 737 insertions(+), 9 deletions(-) create mode 100644 build/build-curriculum.ts create mode 100644 build/curriculum.ts create mode 100644 client/src/assets/icons/cur-ext-resource.svg create mode 100644 client/src/assets/icons/cur-mdn-resource.svg create mode 100644 client/src/assets/icons/cur-resources.svg create mode 100644 client/src/curriculum/module.scss create mode 100644 client/src/curriculum/module.tsx create mode 100644 libs/types/curriculum.ts diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index c74e6035d6c0..c3ca523e35b8 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -89,6 +89,12 @@ jobs: lfs: true token: ${{ secrets.MDN_STUDIO_PAT }} + - uses: actions/checkout@v4 + if: ${{ ! vars.SKIP_BUILD }} + with: + repository: mdn/curriculum + path: mdn/curriculum + # Our usecase is a bit complicated. When the cron schedule runs this workflow, # we rely on the env vars defined at the top of the file. But if it's a manual # trigger we rely on the inputs and only the inputs. That way, the user can @@ -169,6 +175,7 @@ jobs: CONTENT_TRANSLATED_ROOT: ${{ github.workspace }}/mdn/translated-content/files CONTRIBUTOR_SPOTLIGHT_ROOT: ${{ github.workspace }}/mdn/mdn-contributor-spotlight/contributors BLOG_ROOT: ${{ github.workspace }}/mdn/mdn-studio/content/posts + CURRICULUM_ROOT: ${{ github.workspace }}/mdn/curriculum BASE_URL: "https://developer.allizom.org" # The default for this environment variable is geared for writers @@ -271,6 +278,9 @@ jobs: # Build the blog yarn build:blog + # Build the curriculum + yarn build:curriculum + # Generate whatsdeployed files. yarn tool whatsdeployed --output client/build/_whatsdeployed/code.json yarn tool whatsdeployed $CONTENT_ROOT --output client/build/_whatsdeployed/content.json diff --git a/build/build-curriculum.ts b/build/build-curriculum.ts new file mode 100644 index 000000000000..af8f1303f1e2 --- /dev/null +++ b/build/build-curriculum.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { buildCurriculum } from "./curriculum.js"; + +buildCurriculum({ verbose: true }); diff --git a/build/curriculum.ts b/build/curriculum.ts new file mode 100644 index 000000000000..a7a98f39f74f --- /dev/null +++ b/build/curriculum.ts @@ -0,0 +1,276 @@ +import { fdir } from "fdir"; +import { BUILD_OUT_ROOT, CURRICULUM_ROOT } from "../libs/env/index.js"; +import { Doc } from "../libs/types/document.js"; +import { DEFAULT_LOCALE } from "../libs/constants/index.js"; +import * as kumascript from "../kumascript/index.js"; +import LANGUAGES_RAW from "../libs/languages/index.js"; +import { syntaxHighlight } from "./syntax-highlight.js"; +import { + injectLoadingLazyAttributes, + injectNoTranslate, + makeTOC, + postLocalFileLinks, + postProcessCurriculumLinks, + postProcessExternalLinks, + postProcessSmallerHeadingIDs, +} from "./utils.js"; +import { wrapTables } from "./wrap-tables.js"; +import { extractSections } from "./extract-sections.js"; +import path from "node:path"; +import fs from "node:fs/promises"; +import { + CurriculumFrontmatter, + ModuleData, + ModuleMetaData, + SidebarEntry, +} from "../libs/types/curriculum.js"; +import frontmatter from "front-matter"; +import { HydrationData } from "../libs/types/hydration.js"; +import { memoize, slugToFolder } from "../content/utils.js"; +import { renderHTML } from "../ssr/dist/main.js"; + +async function allFiles(): Promise { + const api = new fdir() + .withFullPaths() + .withErrors() + .filter((filePath) => filePath.endsWith(".md")) + .crawl(path.join(CURRICULUM_ROOT, "curriculum")); + return await api.withPromise(); +} + +export const buildIndex = memoize(async () => { + const files = await allFiles(); + const modules = await Promise.all( + files.map( + async (file) => + (await readModule(file, { previousNext: false, sidebar: false })).meta + ) + ); + return modules; +}); + +export async function buildSidebar(): Promise { + const index = await buildIndex(); + + const s = index.reduce((sidebar, { url, title, slug }) => { + const currentLvl = slug.split("/").length; + const last = sidebar.length ? sidebar[sidebar.length - 1] : null; + const entry = { + url, + title, + slug, + }; + if (currentLvl > 2) { + if (last) { + last.children.push(entry); + return sidebar; + } + } + + sidebar.push({ children: [], ...entry }); + return sidebar; + }, []); + + return s; +} + +async function readModule( + file: string, + options?: { + previousNext?: boolean; + sidebar?: boolean; + } +): Promise<{ meta: ModuleMetaData; body: string; sidebar: any }> { + const raw = await fs.readFile(file, "utf-8"); + const { attributes, body: rawBody } = frontmatter(raw); + const filename = file.replace(CURRICULUM_ROOT, "").replace(/^\/?/, ""); + const title = rawBody.match(/^[\w\n]*#+(.*\n)/)[1]?.trim(); + const body = rawBody.replace(/^[\w\n]*#+(.*\n)/, ""); + + const slug = filename.replace(/\.md$/, "").replace("/0-README", ""); + const url = `/${DEFAULT_LOCALE}/${slug}/`; + + const sidebar = options?.sidebar && (await buildSidebar()); + + return { + meta: { filename, slug, url, title, ...attributes }, + sidebar, + body, + }; +} + +export async function findModuleBySlug( + slug: string +): Promise { + let slugPath = `${slug}.md`.split("/"); + let file = path.join(CURRICULUM_ROOT, "curriculum", ...slugPath); + let module; + try { + module = await readModule(file, { sidebar: true }); + } catch { + slugPath = `${slug}/0-README.md`.split("/"); + file = path.join(CURRICULUM_ROOT, "curriculum", ...slugPath); + try { + module = await readModule(file, { sidebar: true }); + } catch { + console.error(`No file found for ${slug}`); + } + } + const { body, meta, sidebar } = module; + + const d = { + url: meta.url, + rawBody: body, + metadata: { locale: DEFAULT_LOCALE, ...meta }, + isMarkdown: true, + fileInfo: { + path: file, + }, + sidebar, + }; + + const doc = await buildModule(d); + return { doc, curriculumMeta: meta }; +} + +export async function buildModule(document: any): Promise { + const { metadata, sidebar } = document; + + const doc = { locale: DEFAULT_LOCALE } as Partial; + let $ = null; + + [$] = await kumascript.render(document.url, {}, document as any); + + $("[data-token]").removeAttr("data-token"); + $("[data-flaw-src]").removeAttr("data-flaw-src"); + + doc.title = metadata.title || ""; + doc.mdn_url = document.url; + doc.locale = metadata.locale as string; + doc.native = LANGUAGES_RAW[DEFAULT_LOCALE]?.native; + + if ($("math").length > 0) { + doc.hasMathML = true; + } + $("div.hidden").remove(); + syntaxHighlight($, doc); + injectNoTranslate($); + injectLoadingLazyAttributes($); + postProcessCurriculumLinks($, document.url); + postProcessExternalLinks($); + postLocalFileLinks($, doc); + postProcessSmallerHeadingIDs($); + wrapTables($); + setCurriculumTypes($); + try { + const [sections] = await extractSections($); + doc.body = sections; + } catch (error) { + console.error( + `Extracting sections failed in ${doc.mdn_url} (${document.fileInfo.path})` + ); + throw error; + } + + doc.pageTitle = `${doc.title} | MDN Blog`; + + doc.noIndexing = false; + doc.toc = makeTOC(doc, true); + doc.sidebar = sidebar; + + return doc as Doc; +} + +export async function buildCurriculum(options: { + verbose?: boolean; + noIndexing?: boolean; +}) { + const locale = DEFAULT_LOCALE; + + for (const file of await allFiles()) { + console.log(`building: ${file}`); + + const { meta, body, sidebar } = await readModule(file, { sidebar: true }); + + const url = meta.url; + const renderUrl = url.replace(/\/$/, ""); + const renderDoc = { + url: renderUrl, + rawBody: body, + metadata: { locale, ...meta }, + isMarkdown: true, + fileInfo: { + path: file, + }, + }; + const builtDoc = await buildModule(renderDoc); + const { doc } = { + doc: { ...builtDoc, summary: meta.summary, mdn_url: url, sidebar }, + }; + + const context: HydrationData = { + doc, + pageTitle: meta.title, + locale, + noIndexing: options.noIndexing, + }; + + const outPath = path.join( + BUILD_OUT_ROOT, + locale.toLowerCase(), + slugToFolder(meta.slug) + ); + + await fs.mkdir(outPath, { recursive: true }); + + const html = renderHTML(`/${locale}/${meta.slug}/`, context); + + const filePath = path.join(outPath, "index.html"); + const jsonFilePath = path.join(outPath, "index.json"); + + await fs.mkdir(outPath, { recursive: true }); + await fs.writeFile(filePath, html); + await fs.writeFile(jsonFilePath, JSON.stringify(context)); + + if (options.verbose) { + console.log("Wrote", filePath); + } + } +} + +function setCurriculumTypes($) { + $("p").each((_, child) => { + const p = $(child); + const text = p.text(); + switch (text) { + case "Learning outcomes:": + p.addClass("curriculum-outcomes"); + break; + case "General resources:": + case "Resources:": + p.addClass("curriculum-resources"); + break; + } + }); + + $("p.curriculum-resources + ul > li").each((_, child) => { + const li = $(child); + + if (li.find("a.external").length) { + li.addClass("external"); + } + }); + + $("blockquote").each((_, child) => { + const bq = $(child); + + const [p] = bq.find("p"); + + if (p) { + const notes = $(p); + if (notes.text() === "Notes:") { + bq.addClass("curriculum-notes"); + } + } + }); +} diff --git a/build/utils.ts b/build/utils.ts index 86750a18e201..b5fc50fd7d8f 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -239,6 +239,28 @@ export function postProcessExternalLinks($) { }); } +/** + * For every `` remove the ".md" + * + * @param {Cheerio document instance} $ + * @param {current url} url + */ +export function postProcessCurriculumLinks($, url) { + $("a[href^=./]").each((_, element) => { + // Expand relative links (TODO: fix) + const $a = $(element); + $a.attr("href", $a.attr("href").replace(/^\.\//, `${url}`)); + }); + $("a[href^=/en-US/curriculum]").each((_, element) => { + const $a = $(element); + $a.attr("href", $a.attr("href").replace(/(.*)\.md(#.*|$)/, "$1/$2")); + }); + $("a[href^=/curriculum]").each((_, element) => { + const $a = $(element); + $a.attr("href", $a.attr("href").replace(/(.*)\.md(#.*|$)/, "/en-US$1/$2")); + }); +} + /** * For every ``, where 'THING' is not a http or / link, make it * `` @@ -294,16 +316,17 @@ export function postProcessSmallerHeadingIDs($) { * * @param {Document} doc */ -export function makeTOC(doc) { +export function makeTOC(doc, withH3 = false) { return doc.body .map((section) => { if ( - (section.type === "prose" || + ((section.type === "prose" || section.type === "browser_compatibility" || section.type === "specifications") && - section.value.id && - section.value.title && - !section.value.isH3 + section.value.id && + section.value.title && + !section.value.isH3) || + withH3 ) { return { text: section.value.title, id: section.value.id }; } diff --git a/client/src/app.tsx b/client/src/app.tsx index 243208bed85e..0ac9c10bdd19 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -28,6 +28,7 @@ import { HydrationData } from "../../libs/types/hydration"; import { TopPlacement } from "./ui/organisms/placement"; import { Blog } from "./blog"; import { Newsletter } from "./newsletter"; +import { CurriculumModule } from "./curriculum/module"; const AllFlaws = React.lazy(() => import("./flaws")); const Translations = React.lazy(() => import("./translations")); @@ -164,6 +165,14 @@ export function App(appProps: HydrationData) { time it hits any React code. */} + + + + } + /> + + + + + + + + + diff --git a/client/src/assets/icons/cur-mdn-resource.svg b/client/src/assets/icons/cur-mdn-resource.svg new file mode 100644 index 000000000000..d327580cfb25 --- /dev/null +++ b/client/src/assets/icons/cur-mdn-resource.svg @@ -0,0 +1,8 @@ + + + + diff --git a/client/src/assets/icons/cur-resources.svg b/client/src/assets/icons/cur-resources.svg new file mode 100644 index 000000000000..67042f7e1189 --- /dev/null +++ b/client/src/assets/icons/cur-resources.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss new file mode 100644 index 000000000000..160b17740fc6 --- /dev/null +++ b/client/src/curriculum/module.scss @@ -0,0 +1,219 @@ +@use "../ui/vars" as *; + +.curriculum-content-container { + h1, + h2, + h3, + h4, + h5, + h6 { + a:link, + a:visited { + color: var(--text-primary); + text-decoration: none; + } + + a:hover, + a:focus { + text-decoration: underline; + } + + a:active { + background-color: transparent; + } + + a[href^="#"] { + &::before { + color: var(--text-inactive); + content: "#"; + display: inline-block; + font-size: 0.7em; + line-height: 1; + margin-left: -0.8em; + text-decoration: none; + visibility: hidden; + width: 0.8em; + } + + &:hover { + &::before { + visibility: visible; + } + } + } + } + + .sidebar, + .toc, + .curriculum-content { + padding-bottom: 3rem; + padding-top: 3rem; + } + + .sidebar { + align-self: start; + grid-area: sidebar; + + li { + margin-left: 1rem; + } + } + + .curriculum-content { + grid-area: main; + + blockquote.curriculum-notes { + border: 0px; + border-radius: var(--elem-radius); + background-color: #fcefe299; + padding: 1rem 2rem; + margin: 1rem; + } + + p.curriculum-outcomes { + display: flex; + font-weight: var(--font-body-strong-weight); + margin-bottom: 0.5rem; + &::before { + content: url("../assets/icons/cur-resources.svg"); + margin-right: 1rem; + width: 24px; + height: 24px; + display: block; + } + } + + li { + list-style-type: disc; + list-style-position: inside; + margin-left: 1rem; + } + + p.curriculum-resources { + margin-bottom: 0.5rem; + + ul > li { + list-style-type: none; + + &:not(.external)::before { + content: url("../assets/icons/cur-mdn-resource.svg"); + margin-right: 0.5rem; + display: inline-block; + width: 1em; + height: 1em; + } + &.external { + &::before { + content: url("../assets/icons/cur-ext-resource.svg"); + margin-right: 0.5rem; + display: inline-block; + width: 1em; + height: 1em; + } + } + } + } + } + + @media screen and (min-width: $screen-md) { + display: grid; + gap: 3rem; + grid-template-areas: "sidebar main"; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + padding-left: 1.5rem; + padding-right: 3rem; + } + + @media screen and (min-width: $screen-xl) { + display: grid; + gap: 3rem; + grid-template-areas: "sidebar main toc"; + grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax(0, 15rem); + padding-left: 1rem; + padding-right: 1rem; + + .toc { + --offset: var(--sticky-header-with-actions-height); + + display: block; + grid-area: toc; + height: fit-content; + padding-bottom: 0; + } + + .in-nav-toc { + display: none; + } + } + + .sidebar-container { + --offset: var(--sticky-header-with-actions-height); + --max-height: calc(100vh - var(--offset)); + + @media screen and (min-width: $screen-md) and (min-height: $screen-height-place-limit) { + display: flex; + flex-direction: column; + } + + max-height: var(--max-height); + position: sticky; + top: var(--offset); + z-index: var(--z-index-sidebar-mobile); + + @media screen and (min-width: $screen-md) { + z-index: auto; + + .sidebar { + mask-image: linear-gradient( + to bottom, + rgb(0, 0, 0) 0% calc(100% - 3rem), + rgba(0, 0, 0, 0) 100% + ); + } + + @media screen and not (min-height: $screen-height-place-limit) { + overflow: auto; + } + } + + &.toc-container, + .toc-container { + grid-area: toc; + .place { + grid-area: toc; + margin: 0; + } + @media screen and (min-width: $screen-xl) { + display: flex; + flex-direction: column; + gap: 0; + height: calc(100vh - var(--offset)); + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 0%, + rgb(0, 0, 0) 3rem calc(100% - 3rem), + rgba(0, 0, 0, 0) 100% + ); + overflow: auto; + position: sticky; + top: var(--offset); + + .place { + margin: 1rem 0; + padding-bottom: 3rem; + } + } + @media screen and (max-width: #{$screen-md - 1}) { + .place { + display: none; + } + } + } + @media screen and (min-width: $screen-xl) { + display: contents; + + .sidebar { + mask-image: none; + } + } + } +} diff --git a/client/src/curriculum/module.tsx b/client/src/curriculum/module.tsx new file mode 100644 index 000000000000..3a0d8d1449ce --- /dev/null +++ b/client/src/curriculum/module.tsx @@ -0,0 +1,88 @@ +import useSWR from "swr"; +import { HydrationData } from "../../../libs/types/hydration"; +import { ModuleData, SidebarEntry } from "../../../libs/types/curriculum"; +import { HTTPError, RenderDocumentBody } from "../document"; +import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; +import { TOC } from "../document/organisms/toc"; +import { SidePlacement } from "../ui/organisms/placement"; + +import "./module.scss"; + +export function CurriculumModule(props: HydrationData) { + const dataURL = `./index.json`; + const { data } = useSWR( + dataURL, + async (url) => { + const response = await fetch(url); + + if (!response.ok) { + switch (response.status) { + case 404: + throw new HTTPError(response.status, url, "Page not found"); + } + + const text = await response.text(); + throw new HTTPError(response.status, url, text); + } + + return await response.json(); + }, + { + fallbackData: props as ModuleData, + revalidateOnFocus: WRITER_MODE, + revalidateOnMount: !props.blogMeta, + } + ); + const { doc, curriculumMeta } = data || props || {}; + return ( + <> + {doc && ( +
+
+
+ + {PLACEMENT_ENABLED && } +
+ +
+
+
+

{doc?.title}

+ {curriculumMeta?.topic &&

{curriculumMeta.topic}

} +
+ +
+
+ )} + + ); +} + +function Sidebar({ sidebar = [] }: { sidebar: SidebarEntry[] }) { + return ( +
+ ); +} diff --git a/client/src/document/organisms/toc/index.tsx b/client/src/document/organisms/toc/index.tsx index 31a6693473fe..26f3ca38eb5f 100644 --- a/client/src/document/organisms/toc/index.tsx +++ b/client/src/document/organisms/toc/index.tsx @@ -72,13 +72,13 @@ function TOCItem({ currentViewedTocItem, }: Toc & { currentViewedTocItem: string }) { const gleanClick = useGleanClick(); - const href = `#${id.toLowerCase()}`; + const href = id && `#${id.toLowerCase()}`; return (
  • gleanClick(`${TOC_CLICK}: ${href}`)} diff --git a/client/src/placement-context.tsx b/client/src/placement-context.tsx index e4eafb266973..0f562db8532e 100644 --- a/client/src/placement-context.tsx +++ b/client/src/placement-context.tsx @@ -21,7 +21,7 @@ export interface PlacementContextData } const PLACEMENT_MAP: Record = { - side: /\/[^/]+\/(play|docs\/|blog\/|search$)/i, + side: /\/[^/]+\/(play|docs\/|blog\/|curriculum\/|search$)/i, top: /\/[^/]+\/(?!$|_homepage$).*/i, hpMain: /\/[^/]+\/($|_homepage$)/i, hpFooter: /\/[^/]+\/($|_homepage$)/i, diff --git a/client/src/ui/molecules/main-menu/index.tsx b/client/src/ui/molecules/main-menu/index.tsx index 7a8b10e52e32..8cfcef56142b 100644 --- a/client/src/ui/molecules/main-menu/index.tsx +++ b/client/src/ui/molecules/main-menu/index.tsx @@ -82,6 +82,7 @@ export default function MainMenu({ isOpenOnMobile }) { toggleMenu={toggleMenu} /> )} + Curriculum Blog Play diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 04ed76e07541..66019a394427 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -74,7 +74,7 @@ router.get( proxyContent ); router.get( - "/[^/]+/blog($|/*)", + ["/[^/]+/blog($|/*)", "/[^/]+/curriculum($|/*)"], requireOrigin(Origin.main), redirectLocale, redirectEnforceTrailingSlash, diff --git a/deployer/src/deployer/search/__init__.py b/deployer/src/deployer/search/__init__.py index 710f7870e88a..06da5e8ebd79 100644 --- a/deployer/src/deployer/search/__init__.py +++ b/deployer/src/deployer/search/__init__.py @@ -216,6 +216,11 @@ def to_search(file, _index=None): # other SPAs like the home page. Skip these. return doc = data["doc"] + + if doc["mdn_url"].startswith("/en-US/curriculum/"): + # Skip curriculum content for now. + return + locale, slug = doc["mdn_url"].split("/docs/", 1) if slug.endswith("/Index"): # We have a lot of pages that uses the `{{Index(...)}}` kumascript macro diff --git a/libs/env/index.d.ts b/libs/env/index.d.ts index d8c5b3b57e4e..a619ba438f61 100644 --- a/libs/env/index.d.ts +++ b/libs/env/index.d.ts @@ -15,6 +15,7 @@ export const CONTENT_ROOT: string; export const CONTENT_TRANSLATED_ROOT: string; export const CONTRIBUTOR_SPOTLIGHT_ROOT: string; export const BLOG_ROOT: string; +export const CURRICULUM_ROOT: string; export const REPOSITORY_URLS: { [path: string]: string; }; diff --git a/libs/env/index.js b/libs/env/index.js index 9593cd7327a5..22e6282195b2 100644 --- a/libs/env/index.js +++ b/libs/env/index.js @@ -82,6 +82,8 @@ export const CONTRIBUTOR_SPOTLIGHT_ROOT = correctContentPathFromEnv( export const BLOG_ROOT = correctContentPathFromEnv("BLOG_ROOT"); +export const CURRICULUM_ROOT = process.env.CURRICULUM_ROOT; + // This makes it possible to know, give a root folder, what is the name of // the repository on GitHub. // E.g. `'https://github.com/' + REPOSITORY_URLS[document.fileInfo.root]` diff --git a/libs/types/curriculum.ts b/libs/types/curriculum.ts new file mode 100644 index 000000000000..0e7dc5a1084d --- /dev/null +++ b/libs/types/curriculum.ts @@ -0,0 +1,28 @@ +import { Doc } from "./document.js"; + +export enum Topic {} + +export interface SidebarEntry { + url: string; + title: string; + slug: string; + children?: SidebarEntry[]; +} + +export interface CurriculumFrontmatter { + summary?: string; + icon?: string; + topic?: Topic; +} + +export interface ModuleMetaData extends CurriculumFrontmatter { + url: string; + filename: string; + slug: string; + title: string; +} + +export interface ModuleData { + doc: Doc; + curriculumMeta: ModuleMetaData; +} diff --git a/libs/types/document.ts b/libs/types/document.ts index 4aca45f50064..512ddfdf74fb 100644 --- a/libs/types/document.ts +++ b/libs/types/document.ts @@ -1,3 +1,4 @@ +import { SidebarEntry } from "./curriculum.js"; import type { SupportStatus } from "./web-features.js"; export interface Source { @@ -155,6 +156,7 @@ export interface DocMetadata { export interface Doc extends DocMetadata { sidebarHTML: string; sidebarMacro?: string; + sidebar: SidebarEntry[]; toc: Toc[]; body: Section[]; } diff --git a/libs/types/hydration.ts b/libs/types/hydration.ts index 56b8aec0399c..f502ceb7d3b1 100644 --- a/libs/types/hydration.ts +++ b/libs/types/hydration.ts @@ -1,9 +1,11 @@ import { BlogPostMetadata } from "./blog.js"; +import { ModuleMetaData } from "./curriculum.js"; interface HydrationData { hyData?: T; doc?: any; blogMeta?: BlogPostMetadata | null; + curriculumMeta?: ModuleMetaData | null; pageNotFound?: boolean; pageTitle?: any; possibleLocales?: any; diff --git a/package.json b/package.json index a9928caacab8..711a7e339aad 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build": "cross-env NODE_ENV=production NODE_OPTIONS='--no-warnings=ExperimentalWarning --loader ts-node/esm' node build/cli.ts", "build:blog": "cross-env NODE_ENV=production ts-node build/build-blog.ts", "build:client": "cd client && cross-env NODE_ENV=production BABEL_ENV=production INLINE_RUNTIME_CHUNK=false node scripts/build.js", + "build:curriculum": "cross-env NODE_ENV=production ts-node build/build-curriculum.ts", "build:dist": "tsc -p tsconfig.dist.json", "build:glean": "cd client && cross-env VIRTUAL_ENV=venv glean translate src/telemetry/metrics.yaml src/telemetry/pings.yaml -f typescript -o src/telemetry/generated", "build:prepare": "yarn build:client && yarn build:ssr && yarn tool optimize-client-build && yarn tool google-analytics-code && yarn tool popularities && yarn tool spas && yarn tool gather-git-history && yarn tool build-robots-txt", diff --git a/server/index.ts b/server/index.ts index 21539484f193..3f0bb28819cb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -53,6 +53,7 @@ import { findPostBySlug, findPostPathBySlug, } from "../build/blog.js"; +import { buildIndex, findModuleBySlug } from "../build/curriculum.js"; async function buildDocumentFromURL(url: string) { const document = Document.findByURL(url); @@ -236,6 +237,30 @@ app.get("/*/contributors.txt", async (req, res) => { ); }); +//app.get("/:locale/curriculum/index.json", async (req, res) => { +// const data = await buildIndex(); +// if (!data) { +// return res.status(404).send("Nothing here 🤷‍♂️"); +// } +// return res.json(data); +//}); + +app.get( + [ + "/:locale/curriculum/:slug([\\S\\/]+)/index.json", + "/:locale/curriculum/index.json", + ], + async (req, res) => { + const { slug = "" } = req.params; + console.log(slug); + const data = await findModuleBySlug(slug); + if (!data) { + return res.status(404).send("Nothing here 🤷‍♂️"); + } + return res.json(data); + } +); + app.get("/:locale/blog/index.json", async (_, res) => { const posts = await allPostFrontmatter( { includeUnpublished: true }, From 597ef298ca7c39929a5f3f1374405a27faf10714 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Tue, 30 Jan 2024 23:25:02 +0100 Subject: [PATCH 02/28] wip --- build/curriculum.ts | 117 ++++++++++++++++++++--------- build/utils.ts | 4 + client/src/app.tsx | 6 +- client/src/curriculum/index.scss | 9 +++ client/src/curriculum/index.tsx | 86 +++++++++++++++++++++ client/src/curriculum/module.tsx | 30 +------- client/src/curriculum/modules.tsx | 33 ++++++++ client/src/curriculum/overview.tsx | 67 +++++++++++++++++ client/src/curriculum/sidebar.tsx | 27 +++++++ libs/types/curriculum.ts | 15 +++- libs/types/document.ts | 5 +- 11 files changed, 330 insertions(+), 69 deletions(-) create mode 100644 client/src/curriculum/index.scss create mode 100644 client/src/curriculum/index.tsx create mode 100644 client/src/curriculum/modules.tsx create mode 100644 client/src/curriculum/overview.tsx create mode 100644 client/src/curriculum/sidebar.tsx diff --git a/build/curriculum.ts b/build/curriculum.ts index a7a98f39f74f..37a13a0d8ee6 100644 --- a/build/curriculum.ts +++ b/build/curriculum.ts @@ -22,44 +22,65 @@ import { CurriculumFrontmatter, ModuleData, ModuleMetaData, - SidebarEntry, + ModuleIndexEntry, } from "../libs/types/curriculum.js"; import frontmatter from "front-matter"; import { HydrationData } from "../libs/types/hydration.js"; import { memoize, slugToFolder } from "../content/utils.js"; import { renderHTML } from "../ssr/dist/main.js"; -async function allFiles(): Promise { +export const allFiles = memoize(async () => { const api = new fdir() .withFullPaths() .withErrors() .filter((filePath) => filePath.endsWith(".md")) .crawl(path.join(CURRICULUM_ROOT, "curriculum")); - return await api.withPromise(); -} + return (await api.withPromise()).sort(); +}); export const buildIndex = memoize(async () => { const files = await allFiles(); const modules = await Promise.all( files.map( async (file) => - (await readModule(file, { previousNext: false, sidebar: false })).meta + (await readModule(file, { previousNext: false, withIndex: false })).meta ) ); return modules; }); -export async function buildSidebar(): Promise { +export function fileToSlug(file) { + return file + .replace(`${CURRICULUM_ROOT}/`, "") + .replace(/(\d+-|\.md$|\/0?-?README)/g, ""); +} + +export async function slugToFile(slug) { + const all = await allFiles(); + const re = new RegExp( + path.join( + CURRICULUM_ROOT, + "curriculum", + `${slug + .split("/") + .map((x) => String.raw`(\d+-)?${x}`) + .join("/")}.md` + ) + ); + return all.find((x) => { + return re.test(x); + }); +} + +export async function buildModuleIndex( + mapper: (x: ModuleMetaData) => Partial = (x) => x +): Promise { const index = await buildIndex(); - const s = index.reduce((sidebar, { url, title, slug }) => { - const currentLvl = slug.split("/").length; + const s = index.reduce((sidebar, meta) => { + const currentLvl = meta.slug.split("/").length; const last = sidebar.length ? sidebar[sidebar.length - 1] : null; - const entry = { - url, - title, - slug, - }; + const entry = mapper(meta); if (currentLvl > 2) { if (last) { last.children.push(entry); @@ -74,27 +95,52 @@ export async function buildSidebar(): Promise { return s; } +export async function buildSidebar(): Promise { + const index = await buildModuleIndex(({ url, slug, title }) => { + return { url, slug, title }; + }); + + return index; +} + async function readModule( file: string, options?: { previousNext?: boolean; - sidebar?: boolean; + withIndex?: boolean; } -): Promise<{ meta: ModuleMetaData; body: string; sidebar: any }> { +): Promise<{ + meta: ModuleMetaData; + body: string; + sidebar?: ModuleIndexEntry[]; + modules?: ModuleIndexEntry[]; +}> { const raw = await fs.readFile(file, "utf-8"); const { attributes, body: rawBody } = frontmatter(raw); const filename = file.replace(CURRICULUM_ROOT, "").replace(/^\/?/, ""); const title = rawBody.match(/^[\w\n]*#+(.*\n)/)[1]?.trim(); const body = rawBody.replace(/^[\w\n]*#+(.*\n)/, ""); - const slug = filename.replace(/\.md$/, "").replace("/0-README", ""); + const slug = fileToSlug(file); const url = `/${DEFAULT_LOCALE}/${slug}/`; - const sidebar = options?.sidebar && (await buildSidebar()); + const sidebar = options?.withIndex && (await buildSidebar()); + + // For module overview and landing page set modules. + let modules: ModuleIndexEntry[] | undefined; + if (options?.withIndex) { + const index = await buildModuleIndex(); + if (slug === "curriculum") { + modules = index?.filter((x) => x.children?.length); + } else { + modules = index?.find((x) => x.slug === slug)?.children; + } + } return { meta: { filename, slug, url, title, ...attributes }, sidebar, + modules, body, }; } @@ -102,21 +148,18 @@ async function readModule( export async function findModuleBySlug( slug: string ): Promise { - let slugPath = `${slug}.md`.split("/"); - let file = path.join(CURRICULUM_ROOT, "curriculum", ...slugPath); + let file = await slugToFile(slug); + if (!file) { + file = await slugToFile(`${slug}${slug ? "/" : ""}README`); + } let module; try { - module = await readModule(file, { sidebar: true }); - } catch { - slugPath = `${slug}/0-README.md`.split("/"); - file = path.join(CURRICULUM_ROOT, "curriculum", ...slugPath); - try { - module = await readModule(file, { sidebar: true }); - } catch { - console.error(`No file found for ${slug}`); - } + module = await readModule(file, { withIndex: true }); + } catch (e) { + console.error(`No file found for ${slug}`, e); + return; } - const { body, meta, sidebar } = module; + const { body, meta, sidebar, modules } = module; const d = { url: meta.url, @@ -127,6 +170,7 @@ export async function findModuleBySlug( path: file, }, sidebar, + modules, }; const doc = await buildModule(d); @@ -134,7 +178,7 @@ export async function findModuleBySlug( } export async function buildModule(document: any): Promise { - const { metadata, sidebar } = document; + const { metadata, sidebar, modules } = document; const doc = { locale: DEFAULT_LOCALE } as Partial; let $ = null; @@ -172,11 +216,12 @@ export async function buildModule(document: any): Promise { throw error; } - doc.pageTitle = `${doc.title} | MDN Blog`; + doc.pageTitle = `${doc.title} | MDN Curriculum`; doc.noIndexing = false; doc.toc = makeTOC(doc, true); doc.sidebar = sidebar; + doc.modules = modules; return doc as Doc; } @@ -190,7 +235,9 @@ export async function buildCurriculum(options: { for (const file of await allFiles()) { console.log(`building: ${file}`); - const { meta, body, sidebar } = await readModule(file, { sidebar: true }); + const { meta, body, sidebar, modules } = await readModule(file, { + withIndex: true, + }); const url = meta.url; const renderUrl = url.replace(/\/$/, ""); @@ -202,10 +249,12 @@ export async function buildCurriculum(options: { fileInfo: { path: file, }, + sidebar, + modules, }; const builtDoc = await buildModule(renderDoc); const { doc } = { - doc: { ...builtDoc, summary: meta.summary, mdn_url: url, sidebar }, + doc: { ...builtDoc, summary: meta.summary, mdn_url: url }, }; const context: HydrationData = { @@ -268,7 +317,7 @@ function setCurriculumTypes($) { if (p) { const notes = $(p); - if (notes.text() === "Notes:") { + if (/Notes?:/.test(notes.text())) { bq.addClass("curriculum-notes"); } } diff --git a/build/utils.ts b/build/utils.ts index b5fc50fd7d8f..213827cbe290 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -259,6 +259,10 @@ export function postProcessCurriculumLinks($, url) { const $a = $(element); $a.attr("href", $a.attr("href").replace(/(.*)\.md(#.*|$)/, "/en-US$1/$2")); }); + $("a[href^=/en-US/curriculum]").each((_, element) => { + const $a = $(element); + $a.attr("href", $a.attr("href").replace(/\d+-/g, "")); + }); } /** diff --git a/client/src/app.tsx b/client/src/app.tsx index 0ac9c10bdd19..273d614e9e71 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -28,7 +28,7 @@ import { HydrationData } from "../../libs/types/hydration"; import { TopPlacement } from "./ui/organisms/placement"; import { Blog } from "./blog"; import { Newsletter } from "./newsletter"; -import { CurriculumModule } from "./curriculum/module"; +import { Curriculum } from "./curriculum"; const AllFlaws = React.lazy(() => import("./flaws")); const Translations = React.lazy(() => import("./translations")); @@ -168,8 +168,8 @@ export function App(appProps: HydrationData) { - + + } /> diff --git a/client/src/curriculum/index.scss b/client/src/curriculum/index.scss new file mode 100644 index 000000000000..5e633ac21898 --- /dev/null +++ b/client/src/curriculum/index.scss @@ -0,0 +1,9 @@ +.curriculum-content-container { + .curriculum-content { + .modules { + input[type="radio"]:not(:checked) ~ ol { + display: none; + } + } + } +} diff --git a/client/src/curriculum/index.tsx b/client/src/curriculum/index.tsx new file mode 100644 index 000000000000..37a95e4d30f0 --- /dev/null +++ b/client/src/curriculum/index.tsx @@ -0,0 +1,86 @@ +import useSWR from "swr"; +import { Route, Routes } from "react-router-dom"; + +import { HydrationData } from "../../../libs/types/hydration"; +import { ModuleData } from "../../../libs/types/curriculum"; +import { HTTPError, RenderDocumentBody } from "../document"; +import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; +import { TOC } from "../document/organisms/toc"; +import { SidePlacement } from "../ui/organisms/placement"; +import { Sidebar } from "./sidebar"; +import { ModulesListList } from "./modules"; +import { CurriculumModuleOverview } from "./overview"; +import { CurriculumModule } from "./module"; + +import "./index.scss"; + +export function Curriculum(appProps: HydrationData) { + return ( + + } /> + } + /> + } /> + + ); +} + +export function CurriculumLanding(props: HydrationData) { + const dataURL = `./index.json`; + const { data } = useSWR( + dataURL, + async (url) => { + const response = await fetch(url); + + if (!response.ok) { + switch (response.status) { + case 404: + throw new HTTPError(response.status, url, "Page not found"); + } + + const text = await response.text(); + throw new HTTPError(response.status, url, text); + } + + return await response.json(); + }, + { + fallbackData: props as ModuleData, + revalidateOnFocus: WRITER_MODE, + revalidateOnMount: !props.blogMeta, + } + ); + const { doc, curriculumMeta } = data || props || {}; + return ( + <> + {doc && ( +
    +
    +
    + + {PLACEMENT_ENABLED && } +
    + +
    +
    +
    +

    {doc?.title}

    + {curriculumMeta?.topic &&

    {curriculumMeta.topic}

    } +
    + +
    +

    Modules:

    + +
    +
    +
    + )} + + ); +} diff --git a/client/src/curriculum/module.tsx b/client/src/curriculum/module.tsx index 3a0d8d1449ce..6a795cdd9ade 100644 --- a/client/src/curriculum/module.tsx +++ b/client/src/curriculum/module.tsx @@ -1,12 +1,13 @@ import useSWR from "swr"; import { HydrationData } from "../../../libs/types/hydration"; -import { ModuleData, SidebarEntry } from "../../../libs/types/curriculum"; +import { ModuleData } from "../../../libs/types/curriculum"; import { HTTPError, RenderDocumentBody } from "../document"; import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; import { TOC } from "../document/organisms/toc"; import { SidePlacement } from "../ui/organisms/placement"; import "./module.scss"; +import { Sidebar } from "./sidebar"; export function CurriculumModule(props: HydrationData) { const dataURL = `./index.json`; @@ -30,7 +31,7 @@ export function CurriculumModule(props: HydrationData) { { fallbackData: props as ModuleData, revalidateOnFocus: WRITER_MODE, - revalidateOnMount: !props.blogMeta, + revalidateOnMount: !props.curriculumMeta, } ); const { doc, curriculumMeta } = data || props || {}; @@ -61,28 +62,3 @@ export function CurriculumModule(props: HydrationData) { ); } - -function Sidebar({ sidebar = [] }: { sidebar: SidebarEntry[] }) { - return ( -
    - ); -} diff --git a/client/src/curriculum/modules.tsx b/client/src/curriculum/modules.tsx new file mode 100644 index 000000000000..9a5e43509d43 --- /dev/null +++ b/client/src/curriculum/modules.tsx @@ -0,0 +1,33 @@ +import { ModuleIndexEntry } from "../../../libs/types/curriculum"; + +export function ModulesListList({ modules }: { modules: ModuleIndexEntry[] }) { + return ( +
      + {modules.map((c, i) => { + return ( +
    1. + + + {c.children && } +
    2. + ); + })} +
    + ); +} + +export function ModulesList({ modules }: { modules: ModuleIndexEntry[] }) { + return ( +
      + {modules.map((c, j) => { + return ( +
    1. + {c.title} +

      {c.summary}

      +

      {c.topic}

      +
    2. + ); + })} +
    + ); +} diff --git a/client/src/curriculum/overview.tsx b/client/src/curriculum/overview.tsx new file mode 100644 index 000000000000..e32aafbd416f --- /dev/null +++ b/client/src/curriculum/overview.tsx @@ -0,0 +1,67 @@ +import useSWR from "swr"; +import { HydrationData } from "../../../libs/types/hydration"; +import { ModuleData } from "../../../libs/types/curriculum"; +import { HTTPError, RenderDocumentBody } from "../document"; +import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; +import { TOC } from "../document/organisms/toc"; +import { SidePlacement } from "../ui/organisms/placement"; +import { Sidebar } from "./sidebar"; +import { ModulesList } from "./modules"; + +export function CurriculumModuleOverview(props: HydrationData) { + const dataURL = `./index.json`; + const { data } = useSWR( + dataURL, + async (url) => { + const response = await fetch(url); + + if (!response.ok) { + switch (response.status) { + case 404: + throw new HTTPError(response.status, url, "Page not found"); + } + + const text = await response.text(); + throw new HTTPError(response.status, url, text); + } + + return await response.json(); + }, + { + fallbackData: props as ModuleData, + revalidateOnFocus: WRITER_MODE, + revalidateOnMount: !props.blogMeta, + } + ); + const { doc, curriculumMeta } = data || props || {}; + return ( + <> + {doc && ( +
    +
    +
    + + {PLACEMENT_ENABLED && } +
    + +
    +
    +
    +

    {doc?.title}

    + {curriculumMeta?.topic &&

    {curriculumMeta.topic}

    } +
    + +
    +

    Module Contents:

    + +
    +
    +
    + )} + + ); +} diff --git a/client/src/curriculum/sidebar.tsx b/client/src/curriculum/sidebar.tsx new file mode 100644 index 000000000000..d64e06e58c26 --- /dev/null +++ b/client/src/curriculum/sidebar.tsx @@ -0,0 +1,27 @@ +import { ModuleIndexEntry } from "../../../libs/types/curriculum"; + +import "./module.scss"; +export function Sidebar({ sidebar = [] }: { sidebar: ModuleIndexEntry[] }) { + return ( + + ); +} diff --git a/libs/types/curriculum.ts b/libs/types/curriculum.ts index 0e7dc5a1084d..38bec027a543 100644 --- a/libs/types/curriculum.ts +++ b/libs/types/curriculum.ts @@ -1,12 +1,21 @@ import { Doc } from "./document.js"; -export enum Topic {} +export enum Topic { + WebStandards = "Web Standards & Semantics", + Styling = "Styling", + Scripting = "Scripting", + BestPractices = "Best Practices", + Tooling = "Tooling", + None = "", +} -export interface SidebarEntry { +export interface ModuleIndexEntry { url: string; title: string; slug: string; - children?: SidebarEntry[]; + summary?: string; + topic?: Topic; + children?: ModuleIndexEntry[]; } export interface CurriculumFrontmatter { diff --git a/libs/types/document.ts b/libs/types/document.ts index 512ddfdf74fb..b217de7bc17a 100644 --- a/libs/types/document.ts +++ b/libs/types/document.ts @@ -1,4 +1,4 @@ -import { SidebarEntry } from "./curriculum.js"; +import { ModuleIndexEntry } from "./curriculum.js"; import type { SupportStatus } from "./web-features.js"; export interface Source { @@ -156,7 +156,8 @@ export interface DocMetadata { export interface Doc extends DocMetadata { sidebarHTML: string; sidebarMacro?: string; - sidebar: SidebarEntry[]; + sidebar: ModuleIndexEntry[]; + modules: ModuleIndexEntry[]; toc: Toc[]; body: Section[]; } From 714682cd0825c6b389d6b84744bdce24dc98f36f Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 14:50:06 +0100 Subject: [PATCH 03/28] wip --- build/curriculum.ts | 118 +++++++++++++----- .../assets/curriculum/cur-topic-practices.svg | 6 + .../assets/curriculum/cur-topic-scripting.svg | 6 + .../assets/curriculum/cur-topic-standards.svg | 6 + .../assets/curriculum/cur-topic-styling.svg | 5 + .../assets/curriculum/cur-topic-tooling.svg | 6 + client/src/app.tsx | 6 +- client/src/curriculum/index.tsx | 62 +++++---- client/src/curriculum/module.scss | 29 +++-- client/src/curriculum/module.tsx | 67 ++++++---- .../{modules.tsx => modules-list.tsx} | 2 + client/src/curriculum/overview.tsx | 64 ++++++---- client/src/curriculum/topic-icon.tsx | 33 +++++ client/src/document/hooks.ts | 3 + libs/types/curriculum.ts | 45 ++++++- libs/types/document.ts | 3 - libs/types/hydration.ts | 4 +- 17 files changed, 335 insertions(+), 130 deletions(-) create mode 100644 client/public/assets/curriculum/cur-topic-practices.svg create mode 100644 client/public/assets/curriculum/cur-topic-scripting.svg create mode 100644 client/public/assets/curriculum/cur-topic-standards.svg create mode 100644 client/public/assets/curriculum/cur-topic-styling.svg create mode 100644 client/public/assets/curriculum/cur-topic-tooling.svg rename client/src/curriculum/{modules.tsx => modules-list.tsx} (89%) create mode 100644 client/src/curriculum/topic-icon.tsx diff --git a/build/curriculum.ts b/build/curriculum.ts index 37a13a0d8ee6..a0fa3301fae4 100644 --- a/build/curriculum.ts +++ b/build/curriculum.ts @@ -1,6 +1,6 @@ import { fdir } from "fdir"; import { BUILD_OUT_ROOT, CURRICULUM_ROOT } from "../libs/env/index.js"; -import { Doc } from "../libs/types/document.js"; +import { Doc, DocParent } from "../libs/types/document.js"; import { DEFAULT_LOCALE } from "../libs/constants/index.js"; import * as kumascript from "../kumascript/index.js"; import LANGUAGES_RAW from "../libs/languages/index.js"; @@ -23,6 +23,11 @@ import { ModuleData, ModuleMetaData, ModuleIndexEntry, + PrevNext, + Template, + CurriculumDoc, + ReadCurriculum, + BuildData, } from "../libs/types/curriculum.js"; import frontmatter from "front-matter"; import { HydrationData } from "../libs/types/hydration.js"; @@ -103,18 +108,53 @@ export async function buildSidebar(): Promise { return index; } +export async function buildPrevNext(slug: string): Promise { + const index = await buildIndex(); + const i = index.findIndex((x) => x.slug === slug); + return { + prev: i > 0 ? index[i - 1] : undefined, + next: i < index.length - 2 ? index[i + 1] : undefined, + }; +} + +function breadPath(url: string, cur: ModuleIndexEntry[]): DocParent[] | null { + for (const entry of cur) { + if (entry.url === url) { + return [{ uri: entry.url, title: entry.title }]; + } + if (entry.children?.length) { + const found = breadPath(url, entry.children); + if (found) { + return [{ uri: entry.url, title: entry.title }, ...found]; + } + } + } + return null; +} + +export async function buildParents(url: string): Promise { + const index = await buildModuleIndex(({ url, title }) => { + return { url, title }; + }); + const parents = breadPath(url, index); + if (parents) { + const { url, title } = index[0]; + if (parents[0]?.uri !== url) { + return [{ uri: url, title }, ...parents]; + } + return parents; + } + + return []; +} + async function readModule( file: string, options?: { previousNext?: boolean; withIndex?: boolean; } -): Promise<{ - meta: ModuleMetaData; - body: string; - sidebar?: ModuleIndexEntry[]; - modules?: ModuleIndexEntry[]; -}> { +): Promise { const raw = await fs.readFile(file, "utf-8"); const { attributes, body: rawBody } = frontmatter(raw); const filename = file.replace(CURRICULUM_ROOT, "").replace(/^\/?/, ""); @@ -124,23 +164,40 @@ async function readModule( const slug = fileToSlug(file); const url = `/${DEFAULT_LOCALE}/${slug}/`; - const sidebar = options?.withIndex && (await buildSidebar()); + let sidebar: ModuleIndexEntry[]; + let parents: DocParent[]; // For module overview and landing page set modules. - let modules: ModuleIndexEntry[] | undefined; + let modules: ModuleIndexEntry[]; + let prevNext: PrevNext; if (options?.withIndex) { - const index = await buildModuleIndex(); - if (slug === "curriculum") { - modules = index?.filter((x) => x.children?.length); - } else { - modules = index?.find((x) => x.slug === slug)?.children; + if (attributes.template === Template.landing) { + modules = (await buildModuleIndex())?.filter((x) => x.children?.length); + } else if (attributes.template === Template.overview) { + modules = (await buildModuleIndex())?.find( + (x) => x.slug === slug + )?.children; + } + if (attributes.template === Template.module) { + prevNext = await buildPrevNext(slug); } + + sidebar = await buildSidebar(); + parents = await buildParents(url); } return { - meta: { filename, slug, url, title, ...attributes }, - sidebar, - modules, + meta: { + filename, + slug, + url, + title, + sidebar, + modules, + parents, + prevNext, + ...attributes, + }, body, }; } @@ -159,9 +216,9 @@ export async function findModuleBySlug( console.error(`No file found for ${slug}`, e); return; } - const { body, meta, sidebar, modules } = module; + const { body, meta } = module; - const d = { + const d: BuildData = { url: meta.url, rawBody: body, metadata: { locale: DEFAULT_LOCALE, ...meta }, @@ -169,18 +226,16 @@ export async function findModuleBySlug( fileInfo: { path: file, }, - sidebar, - modules, }; const doc = await buildModule(d); - return { doc, curriculumMeta: meta }; + return { doc }; } -export async function buildModule(document: any): Promise { - const { metadata, sidebar, modules } = document; +export async function buildModule(document: BuildData): Promise { + const { metadata } = document; - const doc = { locale: DEFAULT_LOCALE } as Partial; + const doc = { locale: DEFAULT_LOCALE } as Partial; let $ = null; [$] = await kumascript.render(document.url, {}, document as any); @@ -220,8 +275,11 @@ export async function buildModule(document: any): Promise { doc.noIndexing = false; doc.toc = makeTOC(doc, true); - doc.sidebar = sidebar; - doc.modules = modules; + doc.sidebar = metadata.sidebar; + doc.modules = metadata.modules; + doc.prevNext = metadata.prevNext; + doc.parents = metadata.parents; + doc.topic = metadata.topic; return doc as Doc; } @@ -235,13 +293,13 @@ export async function buildCurriculum(options: { for (const file of await allFiles()) { console.log(`building: ${file}`); - const { meta, body, sidebar, modules } = await readModule(file, { + const { meta, body } = await readModule(file, { withIndex: true, }); const url = meta.url; const renderUrl = url.replace(/\/$/, ""); - const renderDoc = { + const renderDoc: BuildData = { url: renderUrl, rawBody: body, metadata: { locale, ...meta }, @@ -249,8 +307,6 @@ export async function buildCurriculum(options: { fileInfo: { path: file, }, - sidebar, - modules, }; const builtDoc = await buildModule(renderDoc); const { doc } = { diff --git a/client/public/assets/curriculum/cur-topic-practices.svg b/client/public/assets/curriculum/cur-topic-practices.svg new file mode 100644 index 000000000000..5e8e3f6e57c9 --- /dev/null +++ b/client/public/assets/curriculum/cur-topic-practices.svg @@ -0,0 +1,6 @@ + + + + diff --git a/client/public/assets/curriculum/cur-topic-scripting.svg b/client/public/assets/curriculum/cur-topic-scripting.svg new file mode 100644 index 000000000000..afc18e11a523 --- /dev/null +++ b/client/public/assets/curriculum/cur-topic-scripting.svg @@ -0,0 +1,6 @@ + + + + diff --git a/client/public/assets/curriculum/cur-topic-standards.svg b/client/public/assets/curriculum/cur-topic-standards.svg new file mode 100644 index 000000000000..3dd7e8e457ba --- /dev/null +++ b/client/public/assets/curriculum/cur-topic-standards.svg @@ -0,0 +1,6 @@ + + + + diff --git a/client/public/assets/curriculum/cur-topic-styling.svg b/client/public/assets/curriculum/cur-topic-styling.svg new file mode 100644 index 000000000000..cd69f02e1fe4 --- /dev/null +++ b/client/public/assets/curriculum/cur-topic-styling.svg @@ -0,0 +1,5 @@ + + + + diff --git a/client/public/assets/curriculum/cur-topic-tooling.svg b/client/public/assets/curriculum/cur-topic-tooling.svg new file mode 100644 index 000000000000..b9a306d2f273 --- /dev/null +++ b/client/public/assets/curriculum/cur-topic-tooling.svg @@ -0,0 +1,6 @@ + + + + diff --git a/client/src/app.tsx b/client/src/app.tsx index 273d614e9e71..4dc6121f0d9c 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -55,7 +55,7 @@ function Layout({ pageType, children }) { } ${pageType}`} > - {pageType !== "document-page" && ( + {pageType !== "document-page" && pageType !== "curriculum" && (
    @@ -168,9 +168,9 @@ export function App(appProps: HydrationData) { + - + } /> ) { const dataURL = `./index.json`; const { data } = useSWR( dataURL, @@ -52,34 +54,40 @@ export function CurriculumLanding(props: HydrationData) { revalidateOnMount: !props.blogMeta, } ); - const { doc, curriculumMeta } = data || props || {}; + const { doc }: { doc?: CurriculumDoc } = data || props || {}; return ( <> {doc && ( -
    -
    -
    - - {PLACEMENT_ENABLED && } -
    - + <> +
    + +
    -
    -
    -

    {doc?.title}

    - {curriculumMeta?.topic &&

    {curriculumMeta.topic}

    } -
    - -
    -

    Modules:

    - -
    -
    -
    +
    +
    +
    + + {PLACEMENT_ENABLED && } +
    + {doc.sidebar && } +
    +
    +
    +

    {doc?.title}

    + {doc?.topic &&

    {doc.topic}

    } +
    + +
    +

    Modules:

    + {doc.modules && } +
    +
    +
    + )} ); diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index 160b17740fc6..264b4227e946 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -62,52 +62,60 @@ .curriculum-content { grid-area: main; + .topic-icon { + height: 4rem; + width: 4rem; + } + blockquote.curriculum-notes { - border: 0px; - border-radius: var(--elem-radius); background-color: #fcefe299; - padding: 1rem 2rem; + border: 0; + border-radius: var(--elem-radius); margin: 1rem; + padding: 1rem 2rem; } p.curriculum-outcomes { display: flex; font-weight: var(--font-body-strong-weight); margin-bottom: 0.5rem; + &::before { content: url("../assets/icons/cur-resources.svg"); + display: block; + height: 24px; margin-right: 1rem; width: 24px; - height: 24px; - display: block; } } li { - list-style-type: disc; list-style-position: inside; + list-style-type: disc; margin-left: 1rem; } p.curriculum-resources { margin-bottom: 0.5rem; + + ul > li { list-style-type: none; &:not(.external)::before { content: url("../assets/icons/cur-mdn-resource.svg"); - margin-right: 0.5rem; display: inline-block; - width: 1em; height: 1em; + margin-right: 0.5rem; + width: 1em; } + &.external { &::before { content: url("../assets/icons/cur-ext-resource.svg"); - margin-right: 0.5rem; display: inline-block; - width: 1em; height: 1em; + margin-right: 0.5rem; + width: 1em; } } } @@ -178,6 +186,7 @@ &.toc-container, .toc-container { grid-area: toc; + .place { grid-area: toc; margin: 0; diff --git a/client/src/curriculum/module.tsx b/client/src/curriculum/module.tsx index 6a795cdd9ade..0efc51ba478a 100644 --- a/client/src/curriculum/module.tsx +++ b/client/src/curriculum/module.tsx @@ -1,15 +1,17 @@ import useSWR from "swr"; import { HydrationData } from "../../../libs/types/hydration"; -import { ModuleData } from "../../../libs/types/curriculum"; +import { CurriculumDoc, ModuleData } from "../../../libs/types/curriculum"; import { HTTPError, RenderDocumentBody } from "../document"; import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; import { TOC } from "../document/organisms/toc"; import { SidePlacement } from "../ui/organisms/placement"; - import "./module.scss"; import { Sidebar } from "./sidebar"; +import { TopNavigation } from "../ui/organisms/top-navigation"; +import { ArticleActionsContainer } from "../ui/organisms/article-actions-container"; +import { TopicIcon } from "./topic-icon"; -export function CurriculumModule(props: HydrationData) { +export function CurriculumModule(props: HydrationData) { const dataURL = `./index.json`; const { data } = useSWR( dataURL, @@ -34,30 +36,49 @@ export function CurriculumModule(props: HydrationData) { revalidateOnMount: !props.curriculumMeta, } ); - const { doc, curriculumMeta } = data || props || {}; + const { doc }: { doc?: CurriculumDoc } = data || props || {}; return ( <> {doc && ( -
    -
    -
    - - {PLACEMENT_ENABLED && } -
    - + <> +
    + +
    -
    -
    -

    {doc?.title}

    - {curriculumMeta?.topic &&

    {curriculumMeta.topic}

    } -
    - -
    -
    +
    +
    +
    + + {PLACEMENT_ENABLED && } +
    + {doc.sidebar && } +
    + +
    + )} ); diff --git a/client/src/curriculum/modules.tsx b/client/src/curriculum/modules-list.tsx similarity index 89% rename from client/src/curriculum/modules.tsx rename to client/src/curriculum/modules-list.tsx index 9a5e43509d43..8fbc256193b7 100644 --- a/client/src/curriculum/modules.tsx +++ b/client/src/curriculum/modules-list.tsx @@ -1,4 +1,5 @@ import { ModuleIndexEntry } from "../../../libs/types/curriculum"; +import { TopicIcon } from "./topic-icon"; export function ModulesListList({ modules }: { modules: ModuleIndexEntry[] }) { return ( @@ -22,6 +23,7 @@ export function ModulesList({ modules }: { modules: ModuleIndexEntry[] }) { {modules.map((c, j) => { return (
  • + {c.topic && } {c.title}

    {c.summary}

    {c.topic}

    diff --git a/client/src/curriculum/overview.tsx b/client/src/curriculum/overview.tsx index e32aafbd416f..5e57f5ebcbf5 100644 --- a/client/src/curriculum/overview.tsx +++ b/client/src/curriculum/overview.tsx @@ -1,14 +1,18 @@ import useSWR from "swr"; import { HydrationData } from "../../../libs/types/hydration"; -import { ModuleData } from "../../../libs/types/curriculum"; +import { CurriculumDoc, ModuleData } from "../../../libs/types/curriculum"; import { HTTPError, RenderDocumentBody } from "../document"; import { PLACEMENT_ENABLED, WRITER_MODE } from "../env"; import { TOC } from "../document/organisms/toc"; import { SidePlacement } from "../ui/organisms/placement"; import { Sidebar } from "./sidebar"; -import { ModulesList } from "./modules"; +import { ModulesList } from "./modules-list"; +import { TopNavigation } from "../ui/organisms/top-navigation"; +import { ArticleActionsContainer } from "../ui/organisms/article-actions-container"; -export function CurriculumModuleOverview(props: HydrationData) { +export function CurriculumModuleOverview( + props: HydrationData +) { const dataURL = `./index.json`; const { data } = useSWR( dataURL, @@ -33,34 +37,40 @@ export function CurriculumModuleOverview(props: HydrationData) { revalidateOnMount: !props.blogMeta, } ); - const { doc, curriculumMeta } = data || props || {}; + const { doc }: { doc?: CurriculumDoc } = data || props || {}; return ( <> {doc && ( -
    -
    -
    - - {PLACEMENT_ENABLED && } -
    - + <> +
    + +
    -
    -
    -

    {doc?.title}

    - {curriculumMeta?.topic &&

    {curriculumMeta.topic}

    } -
    - -
    -

    Module Contents:

    - -
    -
    -
    +
    +
    +
    + + {PLACEMENT_ENABLED && } +
    + {doc.sidebar && } +
    +
    +
    +

    {doc?.title}

    + {doc?.topic &&

    {doc.topic}

    } +
    + +
    +

    Module Contents:

    + {doc.modules && } +
    +
    +
    + )} ); diff --git a/client/src/curriculum/topic-icon.tsx b/client/src/curriculum/topic-icon.tsx new file mode 100644 index 000000000000..7732a17c788f --- /dev/null +++ b/client/src/curriculum/topic-icon.tsx @@ -0,0 +1,33 @@ +import { ReactComponent as ScriptingSVG } from "../../public/assets/curriculum/cur-topic-scripting.svg"; +import { ReactComponent as ToolingSVG } from "../../public/assets/curriculum/cur-topic-tooling.svg"; +import { ReactComponent as StandardsSVG } from "../../public/assets/curriculum/cur-topic-standards.svg"; +import { ReactComponent as StylingSVG } from "../../public/assets/curriculum/cur-topic-styling.svg"; +import { ReactComponent as PracticesSVG } from "../../public/assets/curriculum/cur-topic-practices.svg"; + +// Using this import fails the build... +//import { Topic } from "../../../libs/types/curriculum"; +enum Topic { + WebStandards = "Web Standards & Semantics", + Styling = "Styling", + Scripting = "Scripting", + BestPractices = "Best Practices", + Tooling = "Tooling", + None = "", +} + +export function TopicIcon({ topic }: { topic: Topic }) { + switch (topic) { + case Topic.WebStandards: + return ; + case Topic.Styling: + return ; + case Topic.Scripting: + return ; + case Topic.Tooling: + return ; + case Topic.BestPractices: + return ; + default: + return <>; + } +} diff --git a/client/src/document/hooks.ts b/client/src/document/hooks.ts index 1d6c9e432103..38c9114cea10 100644 --- a/client/src/document/hooks.ts +++ b/client/src/document/hooks.ts @@ -138,6 +138,9 @@ export function useStickyHeaderHeight() { const header = document.getElementsByClassName( "sticky-header-container" )?.[0]; + if (!header) { + return; + } const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const { height } = entry.contentRect; diff --git a/libs/types/curriculum.ts b/libs/types/curriculum.ts index 38bec027a543..09e1c36ae712 100644 --- a/libs/types/curriculum.ts +++ b/libs/types/curriculum.ts @@ -1,4 +1,4 @@ -import { Doc } from "./document.js"; +import { Doc, DocParent } from "./document.js"; export enum Topic { WebStandards = "Web Standards & Semantics", @@ -9,6 +9,13 @@ export enum Topic { None = "", } +export enum Template { + module = "module", + overview = "overview", + landing = "landing", + about = "about", +} + export interface ModuleIndexEntry { url: string; title: string; @@ -18,9 +25,14 @@ export interface ModuleIndexEntry { children?: ModuleIndexEntry[]; } +export interface PrevNext { + next?: ModuleIndexEntry; + prev?: ModuleIndexEntry; +} + export interface CurriculumFrontmatter { summary?: string; - icon?: string; + template?: Template; topic?: Topic; } @@ -29,9 +41,34 @@ export interface ModuleMetaData extends CurriculumFrontmatter { filename: string; slug: string; title: string; + sidebar: ModuleIndexEntry[]; + modules: ModuleIndexEntry[]; + parents: DocParent[]; + prevNext?: PrevNext; +} + +export interface CurriculumDoc extends Doc { + sidebar?: ModuleIndexEntry[]; + modules?: ModuleIndexEntry[]; + prevNext?: PrevNext; + topic?: Topic; } export interface ModuleData { - doc: Doc; - curriculumMeta: ModuleMetaData; + doc: CurriculumDoc; +} + +export interface ReadCurriculum { + meta: ModuleMetaData; + body: string; +} + +export interface BuildData { + url: string; + rawBody: string; + metadata: { locale: string } & ModuleMetaData; + isMarkdown: true; + fileInfo: { + path: string; + }; } diff --git a/libs/types/document.ts b/libs/types/document.ts index b217de7bc17a..4aca45f50064 100644 --- a/libs/types/document.ts +++ b/libs/types/document.ts @@ -1,4 +1,3 @@ -import { ModuleIndexEntry } from "./curriculum.js"; import type { SupportStatus } from "./web-features.js"; export interface Source { @@ -156,8 +155,6 @@ export interface DocMetadata { export interface Doc extends DocMetadata { sidebarHTML: string; sidebarMacro?: string; - sidebar: ModuleIndexEntry[]; - modules: ModuleIndexEntry[]; toc: Toc[]; body: Section[]; } diff --git a/libs/types/hydration.ts b/libs/types/hydration.ts index f502ceb7d3b1..0141600e2731 100644 --- a/libs/types/hydration.ts +++ b/libs/types/hydration.ts @@ -1,9 +1,9 @@ import { BlogPostMetadata } from "./blog.js"; import { ModuleMetaData } from "./curriculum.js"; -interface HydrationData { +interface HydrationData { hyData?: T; - doc?: any; + doc?: S; blogMeta?: BlogPostMetadata | null; curriculumMeta?: ModuleMetaData | null; pageNotFound?: boolean; From 8479d92e8c04bd5a7ffb688203a56122670d0499 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 15:09:28 +0100 Subject: [PATCH 04/28] add colors --- client/src/curriculum/index.scss | 23 +++++++++++++++++++++++ client/src/curriculum/modules-list.tsx | 14 +++++++++----- client/src/curriculum/topic-icon.tsx | 10 +++++----- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/client/src/curriculum/index.scss b/client/src/curriculum/index.scss index 5e633ac21898..50846e182a30 100644 --- a/client/src/curriculum/index.scss +++ b/client/src/curriculum/index.scss @@ -1,9 +1,32 @@ .curriculum-content-container { + --cur-color: #fcefe2; + --cur-color-topic-standards: #d5f4f5; + --cur-color-topic-styling: #fff8d6; + --cur-color-topic-scripting: #eff5d5; + --cur-color-topic-tooling: #d5e4f5; + --cur-color-topic-practices: #f5dfd5; + .curriculum-content { .modules { input[type="radio"]:not(:checked) ~ ol { display: none; } } + + .module-list-item { + display: flex; + flex-direction: column; + + > header { + display: flex; + flex-direction: column; + align-items: center; + + .topic-icon { + width: 6rem; + height: 6rem; + } + } + } } } diff --git a/client/src/curriculum/modules-list.tsx b/client/src/curriculum/modules-list.tsx index 8fbc256193b7..9fd3225d138b 100644 --- a/client/src/curriculum/modules-list.tsx +++ b/client/src/curriculum/modules-list.tsx @@ -22,11 +22,15 @@ export function ModulesList({ modules }: { modules: ModuleIndexEntry[] }) {
      {modules.map((c, j) => { return ( -
    1. - {c.topic && } - {c.title} -

      {c.summary}

      -

      {c.topic}

      +
    2. +
      + {c.topic && } + {c.title} +
      +
      +

      {c.summary}

      +

      {c.topic}

      +
    3. ); })} diff --git a/client/src/curriculum/topic-icon.tsx b/client/src/curriculum/topic-icon.tsx index 7732a17c788f..fb331526e59f 100644 --- a/client/src/curriculum/topic-icon.tsx +++ b/client/src/curriculum/topic-icon.tsx @@ -18,15 +18,15 @@ enum Topic { export function TopicIcon({ topic }: { topic: Topic }) { switch (topic) { case Topic.WebStandards: - return ; + return ; case Topic.Styling: - return ; + return ; case Topic.Scripting: - return ; + return ; case Topic.Tooling: - return ; + return ; case Topic.BestPractices: - return ; + return ; default: return <>; } From 7b1b8454ca5eba5b6de0e551485f8e35d0f4a7d0 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 15:18:25 +0100 Subject: [PATCH 05/28] fix(curriculum): correct CURRICULUM_ROOT --- libs/env/index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/env/index.js b/libs/env/index.js index 22e6282195b2..09870e4051ef 100644 --- a/libs/env/index.js +++ b/libs/env/index.js @@ -82,7 +82,7 @@ export const CONTRIBUTOR_SPOTLIGHT_ROOT = correctContentPathFromEnv( export const BLOG_ROOT = correctContentPathFromEnv("BLOG_ROOT"); -export const CURRICULUM_ROOT = process.env.CURRICULUM_ROOT; +export const CURRICULUM_ROOT = correctPathFromEnv("CURRICULUM_ROOT"); // This makes it possible to know, give a root folder, what is the name of // the repository on GitHub. @@ -100,12 +100,20 @@ if (CONTENT_TRANSLATED_ROOT) { REPOSITORY_URLS[CONTENT_TRANSLATED_ROOT] = "mdn/translated-content"; } -function correctContentPathFromEnv(envVarName) { +function correctPathFromEnv(envVarName) { let pathName = process.env[envVarName]; if (!pathName) { return; } pathName = fs.realpathSync(pathName); + return pathName; +} + +function correctContentPathFromEnv(envVarName) { + let pathName = correctPathFromEnv(envVarName); + if (!pathName) { + return; + } if ( path.basename(pathName) !== "files" && fs.existsSync(path.join(pathName, "files")) From aa81f67ab03c9c36f571c6dc03989af2db70ab38 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 15:24:39 +0100 Subject: [PATCH 06/28] fix(curriculum): style lists like in content --- client/src/curriculum/module.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index 264b4227e946..7243dabecfb5 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -89,10 +89,15 @@ } } + ol, + ul { + margin: 1rem 0 2rem; + padding-left: 2rem; + } + li { - list-style-position: inside; list-style-type: disc; - margin-left: 1rem; + margin: 0.5rem 0; } p.curriculum-resources { From d89db886ffbb12c94b6f3edf961b77d28d3b1c9a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 15:25:07 +0100 Subject: [PATCH 07/28] fix(curriculum): align learning outcomes with list items --- client/src/curriculum/module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index 7243dabecfb5..9d1ec3a23ac9 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -84,7 +84,7 @@ content: url("../assets/icons/cur-resources.svg"); display: block; height: 24px; - margin-right: 1rem; + margin-right: 0.5rem; width: 24px; } } From 6be7dbc9ccbd48a7010682c6252404e1602cf2cd Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 16:04:23 +0100 Subject: [PATCH 08/28] chore(observatory): add color to toc --- client/src/curriculum/index.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/curriculum/index.scss b/client/src/curriculum/index.scss index 50846e182a30..f2dbc3917f39 100644 --- a/client/src/curriculum/index.scss +++ b/client/src/curriculum/index.scss @@ -1,4 +1,6 @@ .curriculum-content-container { + --background-toc-active: #fcefe2; + --category-color: #e3642a; --cur-color: #fcefe2; --cur-color-topic-standards: #d5f4f5; --cur-color-topic-styling: #fff8d6; @@ -18,13 +20,13 @@ flex-direction: column; > header { + align-items: center; display: flex; flex-direction: column; - align-items: center; .topic-icon { - width: 6rem; height: 6rem; + width: 6rem; } } } From 7948401b136504d529fc9ba28bde4acec5b82774 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 16:04:52 +0100 Subject: [PATCH 09/28] fix(curriculum): remove margin-bottom from last blockquote child --- client/src/curriculum/module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index 9d1ec3a23ac9..f3764bca9784 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -73,6 +73,10 @@ border-radius: var(--elem-radius); margin: 1rem; padding: 1rem 2rem; + + > :last-child { + margin-bottom: 0; + } } p.curriculum-outcomes { From ade8300c440754360e87239a284cd07d9ef0d181 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 16:21:59 +0100 Subject: [PATCH 10/28] move colors --- client/src/curriculum/landing.scss | 86 ++++++++++++++++++++++++++ client/src/curriculum/modules-list.tsx | 6 +- client/src/curriculum/overview.tsx | 2 +- client/src/ui/base/_themes.scss | 26 ++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 client/src/curriculum/landing.scss diff --git a/client/src/curriculum/landing.scss b/client/src/curriculum/landing.scss new file mode 100644 index 000000000000..1737348a3f67 --- /dev/null +++ b/client/src/curriculum/landing.scss @@ -0,0 +1,86 @@ +.curriculum-content-container { + .curriculum-content { + .modules { + input[type="radio"]:not(:checked) ~ ol { + display: none; + } + } + + .module-contents { + h2 { + width: 100%; + } + + ol { + display: flex; + flex-wrap: wrap; + + .module-list-item { + --cur-bg-color-topic: var(--cur-bg-color); + --cur-color-topic: var(--cur-color); + border: 0; + border-radius: var(--elem-radius); + box-shadow: var(--shadow-02); + display: flex; + flex-direction: column; + margin: 0.5rem; + max-width: 100%; + min-width: 10rem; + width: 30%; + + &.topic-standards { + --cur-bg-color-topic: var(--cur-bg-color-topic-standards); + --cur-color-topic: var(--cur-color-topic-standards); + } + + &.topic-styling { + --cur-bg-color-topic: var(--cur-bg-color-topic-styling); + --cur-color-topic: var(--cur-color-topic-styling); + } + + &.topic-scripting { + --cur-bg-color-topic: var(--cur-bg-color-topic-scripting); + --cur-color-topic: var(--cur-color-topic-scripting); + } + + &.topic-tooling { + --cur-bg-color-topic: var(--cur-bg-color-topic-tooling); + --cur-color-topic: var(--cur-color-topic-tooling); + } + + &.topic-practices { + --cur-bg-color-topic: var(--cur-bg-color-topic-practices); + --cur-color-topic: var(--cur-color-topic-practices); + } + + > header { + align-items: center; + background-color: var(--cur-bg-color-topic); + display: flex; + flex-direction: column; + padding: 2rem; + + .topic-icon { + height: 6rem; + width: 6rem; + } + } + + > section { + align-items: center; + display: flex; + flex-direction: column; + font-size: var(--type-smaller-font-size); + height: 100%; + justify-content: space-between; + padding: 0.5rem 2rem; + + p:last-child { + color: var(--cur-color-topic); + } + } + } + } + } + } +} diff --git a/client/src/curriculum/modules-list.tsx b/client/src/curriculum/modules-list.tsx index 9fd3225d138b..73ca4bef752c 100644 --- a/client/src/curriculum/modules-list.tsx +++ b/client/src/curriculum/modules-list.tsx @@ -1,5 +1,6 @@ import { ModuleIndexEntry } from "../../../libs/types/curriculum"; import { TopicIcon } from "./topic-icon"; +import { topic2css } from "./utils"; export function ModulesListList({ modules }: { modules: ModuleIndexEntry[] }) { return ( @@ -22,7 +23,10 @@ export function ModulesList({ modules }: { modules: ModuleIndexEntry[] }) {
        {modules.map((c, j) => { return ( -
      1. +
      2. {c.topic && } {c.title} diff --git a/client/src/curriculum/overview.tsx b/client/src/curriculum/overview.tsx index 5e57f5ebcbf5..752fcfd05c95 100644 --- a/client/src/curriculum/overview.tsx +++ b/client/src/curriculum/overview.tsx @@ -64,7 +64,7 @@ export function CurriculumModuleOverview( {doc?.topic &&

        {doc.topic}

        }
        -
        +

        Module Contents:

        {doc.modules && }
        diff --git a/client/src/ui/base/_themes.scss b/client/src/ui/base/_themes.scss index 183861105850..3fbca813665f 100644 --- a/client/src/ui/base/_themes.scss +++ b/client/src/ui/base/_themes.scss @@ -207,6 +207,19 @@ --baseline-limited-check: #1e8e3e; --baseline-limited-cross: #ea8600; + --cur-bg-color: #fcefe2; + --cur-bg-color-topic-standards: #d5f4f5; + --cur-bg-color-topic-styling: #eff5d5; + --cur-bg-color-topic-scripting: #fff8d6; + --cur-bg-color-topic-tooling: #d5e4f5; + --cur-bg-color-topic-practices: #f5dfd5; + --cur-color: #fcefe2; + --cur-color-topic-standards: #187b7f; + --cur-color-topic-styling: #187f22; + --cur-color-topic-scripting: #7f6f16; + --cur-color-topic-tooling: #182f7f; + --cur-color-topic-practices: #a25e3f; + color-scheme: light; } @@ -414,6 +427,19 @@ --baseline-limited-check: #1e8e3e; --baseline-limited-cross: #ea8600; + --cur-color: #fcefe2; + --cur-color-topic-standards: #d5f4f5; + --cur-color-topic-styling: #eff5d5; + --cur-color-topic-scripting: #fff8d6; + --cur-color-topic-tooling: #d5e4f5; + --cur-color-topic-practices: #f5dfd5; + --cur-bg-color: #fcefe2; + --cur-bg-color-topic-standards: #187b7f; + --cur-bg-color-topic-styling: #187f22; + --cur-bg-color-topic-scripting: #7f6f16; + --cur-bg-color-topic-tooling: #182f7f; + --cur-bg-color-topic-practices: #a25e3f; + color-scheme: dark; } From 389caede59731960ecd08812fd8e796cb87486be Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 16:23:52 +0100 Subject: [PATCH 11/28] fix modules-list --- .../{landing.scss => modules-list.scss} | 0 client/src/curriculum/modules-list.tsx | 2 ++ client/src/curriculum/utils.ts | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+) rename client/src/curriculum/{landing.scss => modules-list.scss} (100%) create mode 100644 client/src/curriculum/utils.ts diff --git a/client/src/curriculum/landing.scss b/client/src/curriculum/modules-list.scss similarity index 100% rename from client/src/curriculum/landing.scss rename to client/src/curriculum/modules-list.scss diff --git a/client/src/curriculum/modules-list.tsx b/client/src/curriculum/modules-list.tsx index 73ca4bef752c..d456fa520c2b 100644 --- a/client/src/curriculum/modules-list.tsx +++ b/client/src/curriculum/modules-list.tsx @@ -2,6 +2,8 @@ import { ModuleIndexEntry } from "../../../libs/types/curriculum"; import { TopicIcon } from "./topic-icon"; import { topic2css } from "./utils"; +import "./modules-list.scss"; + export function ModulesListList({ modules }: { modules: ModuleIndexEntry[] }) { return (
          diff --git a/client/src/curriculum/utils.ts b/client/src/curriculum/utils.ts new file mode 100644 index 000000000000..d73e1dd26bf3 --- /dev/null +++ b/client/src/curriculum/utils.ts @@ -0,0 +1,26 @@ +// Using this import fails the build... +//import { Topic } from "../../../libs/types/curriculum"; +export enum Topic { + WebStandards = "Web Standards & Semantics", + Styling = "Styling", + Scripting = "Scripting", + BestPractices = "Best Practices", + Tooling = "Tooling", + None = "", +} +export function topic2css(topic?: Topic) { + switch (topic) { + case Topic.WebStandards: + return "standards"; + case Topic.Styling: + return "styling"; + case Topic.Scripting: + return "scripting"; + case Topic.Tooling: + return "tooling"; + case Topic.BestPractices: + return "practices"; + default: + return "none"; + } +} From 1dd9d60f1f9754a75ad05f272fe4e71c09fbfbe6 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 16:25:43 +0100 Subject: [PATCH 12/28] cleanup --- client/src/curriculum/index.scss | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/client/src/curriculum/index.scss b/client/src/curriculum/index.scss index f2dbc3917f39..d00801faa69e 100644 --- a/client/src/curriculum/index.scss +++ b/client/src/curriculum/index.scss @@ -1,12 +1,6 @@ .curriculum-content-container { --background-toc-active: #fcefe2; --category-color: #e3642a; - --cur-color: #fcefe2; - --cur-color-topic-standards: #d5f4f5; - --cur-color-topic-styling: #fff8d6; - --cur-color-topic-scripting: #eff5d5; - --cur-color-topic-tooling: #d5e4f5; - --cur-color-topic-practices: #f5dfd5; .curriculum-content { .modules { @@ -14,21 +8,5 @@ display: none; } } - - .module-list-item { - display: flex; - flex-direction: column; - - > header { - align-items: center; - display: flex; - flex-direction: column; - - .topic-icon { - height: 6rem; - width: 6rem; - } - } - } } } From 1f937d2c01ad3b7c8e952da026dd1fc69542c878 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 16:36:11 +0100 Subject: [PATCH 13/28] chore(curriculum): style header --- client/src/curriculum/module.scss | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index f3764bca9784..26fe2b26daeb 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -62,9 +62,32 @@ .curriculum-content { grid-area: main; - .topic-icon { - height: 4rem; - width: 4rem; + header { + align-items: center; + display: grid; + grid-template-areas: "icon heading" "nothing category"; + justify-content: flex-start; + justify-items: flex-start; + + .topic-icon { + background-color: #eee; + grid-area: icon; + height: 4rem; + width: 4rem; + } + h1 { + grid-area: heading; + margin-bottom: 0rem; + } + p { + font-weight: 400; + grid-area: category; + margin: 0; + + &::before { + content: "Category: "; + } + } } blockquote.curriculum-notes { From 6f92d66774c1c148ebba7d113f74b99c8ac456cf Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 16:40:24 +0100 Subject: [PATCH 14/28] fix(curriculum): align resources items with other items --- client/src/curriculum/module.scss | 33 +++++++++++++++++-------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index 26fe2b26daeb..368a7ba59315 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -130,24 +130,27 @@ p.curriculum-resources { margin-bottom: 0.5rem; - + ul > li { - list-style-type: none; - - &:not(.external)::before { - content: url("../assets/icons/cur-mdn-resource.svg"); - display: inline-block; - height: 1em; - margin-right: 0.5rem; - width: 1em; - } + + ul { + padding-left: 0.25rem; + > li { + list-style-type: none; - &.external { &::before { - content: url("../assets/icons/cur-ext-resource.svg"); display: inline-block; - height: 1em; - margin-right: 0.5rem; - width: 1em; + height: 1.5rem; + margin-right: 0.25rem; + width: 1.5rem; + vertical-align: bottom; + } + + &:not(.external)::before { + content: url("../assets/icons/cur-mdn-resource.svg"); + } + + &.external { + &::before { + content: url("../assets/icons/cur-ext-resource.svg"); + } } } } From 2fe98064ccfba74174bcf073890447992808b7cc Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 16:49:49 +0100 Subject: [PATCH 15/28] svg colors --- build/curriculum.ts | 14 ++++++++------ client/src/curriculum/modules-list.scss | 13 +++++++++++-- client/src/curriculum/topic-icon.scss | 9 +++++++++ client/src/curriculum/topic-icon.tsx | 2 ++ 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 client/src/curriculum/topic-icon.scss diff --git a/build/curriculum.ts b/build/curriculum.ts index a0fa3301fae4..7a13c96100ad 100644 --- a/build/curriculum.ts +++ b/build/curriculum.ts @@ -48,7 +48,7 @@ export const buildIndex = memoize(async () => { const modules = await Promise.all( files.map( async (file) => - (await readModule(file, { previousNext: false, withIndex: false })).meta + (await readModule(file, { previousNext: false, forIndex: true })).meta ) ); return modules; @@ -152,13 +152,13 @@ async function readModule( file: string, options?: { previousNext?: boolean; - withIndex?: boolean; + forIndex?: boolean; } ): Promise { const raw = await fs.readFile(file, "utf-8"); const { attributes, body: rawBody } = frontmatter(raw); const filename = file.replace(CURRICULUM_ROOT, "").replace(/^\/?/, ""); - const title = rawBody.match(/^[\w\n]*#+(.*\n)/)[1]?.trim(); + let title = rawBody.match(/^[\w\n]*#+(.*\n)/)[1]?.trim(); const body = rawBody.replace(/^[\w\n]*#+(.*\n)/, ""); const slug = fileToSlug(file); @@ -170,7 +170,7 @@ async function readModule( // For module overview and landing page set modules. let modules: ModuleIndexEntry[]; let prevNext: PrevNext; - if (options?.withIndex) { + if (!options?.forIndex) { if (attributes.template === Template.landing) { modules = (await buildModuleIndex())?.filter((x) => x.children?.length); } else if (attributes.template === Template.overview) { @@ -184,6 +184,8 @@ async function readModule( sidebar = await buildSidebar(); parents = await buildParents(url); + } else { + title = title.replace(/^\d+\s+/, ""); } return { @@ -211,7 +213,7 @@ export async function findModuleBySlug( } let module; try { - module = await readModule(file, { withIndex: true }); + module = await readModule(file, { forIndex: false }); } catch (e) { console.error(`No file found for ${slug}`, e); return; @@ -294,7 +296,7 @@ export async function buildCurriculum(options: { console.log(`building: ${file}`); const { meta, body } = await readModule(file, { - withIndex: true, + forIndex: false, }); const url = meta.url; diff --git a/client/src/curriculum/modules-list.scss b/client/src/curriculum/modules-list.scss index 1737348a3f67..3dfe32244b4d 100644 --- a/client/src/curriculum/modules-list.scss +++ b/client/src/curriculum/modules-list.scss @@ -58,12 +58,20 @@ background-color: var(--cur-bg-color-topic); display: flex; flex-direction: column; - padding: 2rem; + height: 20rem; + padding: 1rem; .topic-icon { height: 6rem; width: 6rem; } + + > a { + color: var(--text-primary); + font-weight: var(--font-body-strong-weight); + margin-top: 1rem; + text-align: center; + } } > section { @@ -73,10 +81,11 @@ font-size: var(--type-smaller-font-size); height: 100%; justify-content: space-between; - padding: 0.5rem 2rem; + padding: 0.5rem 1rem; p:last-child { color: var(--cur-color-topic); + text-align: center; } } } diff --git a/client/src/curriculum/topic-icon.scss b/client/src/curriculum/topic-icon.scss new file mode 100644 index 000000000000..41ca11cfbb23 --- /dev/null +++ b/client/src/curriculum/topic-icon.scss @@ -0,0 +1,9 @@ +svg.topic-icon { + circle { + fill: var(--background-primary); + } + + path { + fill: var(--cur-color-topic); + } +} diff --git a/client/src/curriculum/topic-icon.tsx b/client/src/curriculum/topic-icon.tsx index fb331526e59f..fdd67d40561f 100644 --- a/client/src/curriculum/topic-icon.tsx +++ b/client/src/curriculum/topic-icon.tsx @@ -4,6 +4,8 @@ import { ReactComponent as StandardsSVG } from "../../public/assets/curriculum/c import { ReactComponent as StylingSVG } from "../../public/assets/curriculum/cur-topic-styling.svg"; import { ReactComponent as PracticesSVG } from "../../public/assets/curriculum/cur-topic-practices.svg"; +import "./topic-icon.scss"; + // Using this import fails the build... //import { Topic } from "../../../libs/types/curriculum"; enum Topic { From 44c1329756b69f1e03b06fc2a01cd606b86242f5 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Wed, 31 Jan 2024 16:41:58 +0100 Subject: [PATCH 16/28] fixup! chore(curriculum): style header --- client/src/curriculum/module.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/curriculum/module.scss b/client/src/curriculum/module.scss index 368a7ba59315..e3b67916c0f2 100644 --- a/client/src/curriculum/module.scss +++ b/client/src/curriculum/module.scss @@ -64,23 +64,25 @@ header { align-items: center; + column-gap: 1.5rem; display: grid; grid-template-areas: "icon heading" "nothing category"; justify-content: flex-start; justify-items: flex-start; .topic-icon { - background-color: #eee; grid-area: icon; height: 4rem; width: 4rem; } + h1 { grid-area: heading; - margin-bottom: 0rem; + margin-bottom: 0; } + p { - font-weight: 400; + font-size: var(--type-smaller-font-size); grid-area: category; margin: 0; @@ -132,6 +134,7 @@ + ul { padding-left: 0.25rem; + > li { list-style-type: none; @@ -139,8 +142,8 @@ display: inline-block; height: 1.5rem; margin-right: 0.25rem; - width: 1.5rem; vertical-align: bottom; + width: 1.5rem; } &:not(.external)::before { From 650baf39448748b405ffac94a6607acfa4a54d79 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Wed, 31 Jan 2024 16:55:54 +0100 Subject: [PATCH 17/28] move colors --- client/src/curriculum/index.scss | 28 +++++++++++++++++++++++++ client/src/curriculum/index.tsx | 5 ++++- client/src/curriculum/module.scss | 2 +- client/src/curriculum/module.tsx | 5 ++++- client/src/curriculum/modules-list.scss | 27 ------------------------ client/src/curriculum/overview.tsx | 5 ++++- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/client/src/curriculum/index.scss b/client/src/curriculum/index.scss index d00801faa69e..85f8d73b65bd 100644 --- a/client/src/curriculum/index.scss +++ b/client/src/curriculum/index.scss @@ -2,6 +2,34 @@ --background-toc-active: #fcefe2; --category-color: #e3642a; + --cur-bg-color-topic: var(--cur-bg-color); + --cur-color-topic: var(--cur-color); + + .topic-standards { + --cur-bg-color-topic: var(--cur-bg-color-topic-standards); + --cur-color-topic: var(--cur-color-topic-standards); + } + + .topic-styling { + --cur-bg-color-topic: var(--cur-bg-color-topic-styling); + --cur-color-topic: var(--cur-color-topic-styling); + } + + .topic-scripting { + --cur-bg-color-topic: var(--cur-bg-color-topic-scripting); + --cur-color-topic: var(--cur-color-topic-scripting); + } + + .topic-tooling { + --cur-bg-color-topic: var(--cur-bg-color-topic-tooling); + --cur-color-topic: var(--cur-color-topic-tooling); + } + + .topic-practices { + --cur-bg-color-topic: var(--cur-bg-color-topic-practices); + --cur-color-topic: var(--cur-color-topic-practices); + } + .curriculum-content { .modules { input[type="radio"]:not(:checked) ~ ol { diff --git a/client/src/curriculum/index.tsx b/client/src/curriculum/index.tsx index ec67b554a2a7..66874eecae95 100644 --- a/client/src/curriculum/index.tsx +++ b/client/src/curriculum/index.tsx @@ -15,6 +15,7 @@ import { CurriculumModule } from "./module"; import "./index.scss"; import { TopNavigation } from "../ui/organisms/top-navigation"; import { ArticleActionsContainer } from "../ui/organisms/article-actions-container"; +import { topic2css } from "./utils"; export function Curriculum(appProps: HydrationData) { return ( @@ -63,7 +64,9 @@ export function CurriculumLanding(props: HydrationData) { -
          +
          -
          +
          -
          +
          {PLACEMENT_ENABLED && }
          - {doc.sidebar && } + {doc.sidebar && ( + + )}
          diff --git a/client/src/curriculum/module.tsx b/client/src/curriculum/module.tsx index af9179576454..35481e98ab35 100644 --- a/client/src/curriculum/module.tsx +++ b/client/src/curriculum/module.tsx @@ -58,7 +58,9 @@ export function CurriculumModule(props: HydrationData) { {PLACEMENT_ENABLED && }
          - {doc.sidebar && } + {doc.sidebar && ( + + )}
          diff --git a/client/src/curriculum/overview.tsx b/client/src/curriculum/overview.tsx index d60acc05dc0d..cf72ab3c68cd 100644 --- a/client/src/curriculum/overview.tsx +++ b/client/src/curriculum/overview.tsx @@ -59,7 +59,9 @@ export function CurriculumModuleOverview( {PLACEMENT_ENABLED && } - {doc.sidebar && } + {doc.sidebar && ( + + )}
          diff --git a/client/src/curriculum/sidebar.tsx b/client/src/curriculum/sidebar.tsx index d64e06e58c26..0c0521c94579 100644 --- a/client/src/curriculum/sidebar.tsx +++ b/client/src/curriculum/sidebar.tsx @@ -1,19 +1,29 @@ import { ModuleIndexEntry } from "../../../libs/types/curriculum"; import "./module.scss"; -export function Sidebar({ sidebar = [] }: { sidebar: ModuleIndexEntry[] }) { +export function Sidebar({ + current = "", + sidebar = [], +}: { + current: string; + sidebar: ModuleIndexEntry[]; +}) { return ( -