diff --git a/src/components/LearningPathCatalog.tsx b/src/components/LearningPathCatalog.tsx new file mode 100644 index 000000000000000..b71cc42be4ec0fa --- /dev/null +++ b/src/components/LearningPathCatalog.tsx @@ -0,0 +1,181 @@ +import { useState, type ChangeEvent } from "react"; +import Markdown from "react-markdown"; +import type { CollectionEntry } from "astro:content"; +import type { IconifyIconBuildResult } from "@iconify/utils"; + +type LearningPaths = CollectionEntry<"learning-paths">["data"][]; +type Icons = Record; + +type Filters = { + products: string[]; + groups: string[]; +}; + +const LearningPathCatalog = ({ + paths, + icons, +}: { + paths: LearningPaths; + icons: Icons; +}) => { + const [filters, setFilters] = useState({ + products: [], + groups: [], + }); + + const sorted = paths.sort((lp1, lp2) => { + return lp1.priority < lp2.priority ? -1 : 1; + }); + + const mapped = sorted.map((lp) => { + const icon = icons[lp.product_group]; + const groups = [lp.product_group]; + + if (lp.additional_groups) { + groups.push(...lp.additional_groups); + } + + return { + title: lp.title, + icon, + link: lp.path, + description: lp.description, + products: lp.products, + groups, + }; + }); + + const products = [...new Set(mapped.flatMap((lp) => lp.products).sort())]; + const groups = [...new Set(mapped.flatMap((lp) => lp.groups).sort())]; + + // apply filters to the fields list + const filtered = mapped.filter((path) => { + if (filters.groups.length > 0) { + if (!path.groups.some((c) => filters.groups.includes(c))) { + return false; + } + } + + if (filters.products.length > 0) { + if (!path.products.some((c) => filters.products.includes(c))) { + return false; + } + } + + return true; + }); + + return ( +
+
+
+ + Product groups + + + {groups.map((group) => ( + + ))} +
+ +
+ + Products + + + {products.map((product) => ( + + ))} +
+
+ +
+ {filtered.length === 0 && ( +
+ No products found +

+ Try a different search term, or broaden your search by removing + filters. +

+
+ )} + {filtered.map((path) => { + return ( + + {path.icon && ( +
+ +
+ )} +

{path.title}

+ + {path.description} + +
+ ); + })} +
+
+ ); +}; + +export default LearningPathCatalog; diff --git a/src/content/learning-paths/cybersafe.json b/src/content/learning-paths/cybersafe.json index 6c461f759513ca8..8087d7865c9ce1d 100644 --- a/src/content/learning-paths/cybersafe.json +++ b/src/content/learning-paths/cybersafe.json @@ -2,7 +2,7 @@ "title": "Project Cybersafe Schools", "path": "/learning-paths/cybersafe/concepts/", "priority": 5, - "description": "Prevent children from accessing obscene or harmful content over the Internet. Go to Project Cybersafe Schools to apply.", + "description": "Prevent children from accessing obscene or harmful content over the Internet. Go to [Project Cybersafe Schools](https://www.cloudflare.com/lp/cybersafe-schools/) to apply.", "products": ["Cloudflare Gateway", "WARP", "DNS"], "product_group": "Cloudflare One", "additional_groups": ["Application security"] diff --git a/src/icons/learning-paths.svg b/src/icons/learning-paths.svg index 4b2640c5dd3500c..a26c29b09d82f6c 100644 --- a/src/icons/learning-paths.svg +++ b/src/icons/learning-paths.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/icons/notifications.svg b/src/icons/notifications.svg index ea7272bf826c8bc..44d3cd404b966f8 100644 --- a/src/icons/notifications.svg +++ b/src/icons/notifications.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/icons/speed.svg b/src/icons/speed.svg index 2d59491f9d25ad4..c401541206a76a5 100644 --- a/src/icons/speed.svg +++ b/src/icons/speed.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/pages/changelog/index.astro b/src/pages/changelog/index.astro index 3a68179a0b5c499..3e6c77a8ceb1024 100644 --- a/src/pages/changelog/index.astro +++ b/src/pages/changelog/index.astro @@ -30,7 +30,7 @@ const props = { { notes.map(async (entry) => { const date = format(entry.data.date, "MMM dd, yyyy"); - const time = format(entry.data.date, "hh:mm a"); + const productIds = JSON.stringify( entry.data.products.map((product) => product.id), ); @@ -41,10 +41,9 @@ const props = {
    diff --git a/src/pages/learning-paths.astro b/src/pages/learning-paths.astro index 514d14d7bcd0eb8..32e08a28e5cafca 100644 --- a/src/pages/learning-paths.astro +++ b/src/pages/learning-paths.astro @@ -1,212 +1,45 @@ --- -import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; -import { getCollection, type CollectionEntry } from "astro:content"; -import { marked } from "marked"; -import { CardGrid, LinkTitleCard, Description } from "~/components"; - -const frontmatter = { - title: "Learning paths", - description: - "Learning paths guide you through modules and projects so you can get started with Cloudflare as quickly as possible.", - template: "splash", -} as const; +import StarlightPage, { + type StarlightPageProps, +} from "@astrojs/starlight/components/StarlightPage.astro"; +import { getCollection } from "astro:content"; +import LearningPathCatalog from "~/components/LearningPathCatalog.tsx"; + +// @ts-expect-error virtual module +import iconCollection from "virtual:astro-icon"; +import { getIconData, iconToSVG } from "@iconify/utils"; + +const props = { + frontmatter: { + title: "Learning paths", + description: + "Learning paths guide you through modules and projects so you can get started with Cloudflare as quickly as possible.", + template: "splash", + }, + hideBreadcrumbs: true, +} as StarlightPageProps; const learningPaths = await getCollection("learning-paths"); -function lpGroups(paths: Array>) { - const groups = paths.flatMap((p) => p.data.product_group ?? []); - const additional = paths.flatMap((p) => p.data.additional_groups ?? []); - - const unique = [...new Set(groups.concat(additional))]; - - return unique.sort(); -} +const data = learningPaths.map((lp) => lp.data); -function lpProducts(paths: Array>) { - const products = paths.flatMap((p) => p.data.products ?? []); +const iconToSvg = (id: string) => { + const data = getIconData(iconCollection.local, id); - const unique = [...new Set(products)]; + if (!data) throw new Error(`Icon ${id} does not exist.`); - return unique.sort(); -} + return iconToSVG(data); +}; -const groups = lpGroups(learningPaths); -const products = lpProducts(learningPaths); +const icons = { + "Cloudflare essentials": iconToSvg("fundamentals"), + "Application performance": iconToSvg("speed"), + "Application security": iconToSvg("ddos-protection"), + "Cloudflare One": iconToSvg("cloudflare-one"), + "Developer platform": iconToSvg("workers"), +}; --- - - - {frontmatter.description} - -
    - -
    - - { - learningPaths - .sort((a, b) => a.data.priority - b.data.priority) - .map((lp) => ( -
    - -
    - )) - } -
    -
    -
    + + - -