Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions src/components/LearningPathCatalog.tsx
Original file line number Diff line number Diff line change
@@ -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<string, IconifyIconBuildResult>;

type Filters = {
products: string[];
groups: string[];
};

const LearningPathCatalog = ({
paths,
icons,
}: {
paths: LearningPaths;
icons: Icons;
}) => {
const [filters, setFilters] = useState<Filters>({
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 (
<div className="md:flex">
<div className="mr-8 w-full md:w-1/4">
<div className="!mb-8 hidden md:block">
<span className="text-sm font-bold uppercase text-gray-600 dark:text-gray-200">
Product groups
</span>

{groups.map((group) => (
<label key={group} className="!my-2 block">
<input
type="checkbox"
className="mr-2"
value={group}
checked={filters.groups.includes(group)}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setFilters({
...filters,
groups: [...filters.groups, e.target.value],
});
} else {
setFilters({
...filters,
groups: filters.groups.filter(
(f) => f !== e.target.value,
),
});
}
}}
/>{" "}
{group}
</label>
))}
</div>

<div className="!mb-8 hidden md:block">
<span className="text-sm font-bold uppercase text-gray-600 dark:text-gray-200">
Products
</span>

{products.map((product) => (
<label key={product} className="!my-2 block">
<input
type="checkbox"
className="mr-2"
value={product}
checked={filters.products.includes(product)}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setFilters({
...filters,
products: [...filters.products, e.target.value],
});
} else {
setFilters({
...filters,
products: filters.products.filter(
(f) => f !== e.target.value,
),
});
}
}}
/>{" "}
{product}
</label>
))}
</div>
</div>

<div className="!mt-0 grid w-full grid-cols-1 items-stretch gap-2 self-start lg:w-3/4 lg:grid-cols-2 lg:gap-4">
{filtered.length === 0 && (
<div className="flex w-full flex-col justify-center rounded-md border bg-gray-50 py-6 text-center align-middle dark:border-gray-500 dark:bg-gray-800 md:col-span-2 lg:col-span-3">
<span className="text-lg !font-bold">No products found</span>
<p>
Try a different search term, or broaden your search by removing
filters.
</p>
</div>
)}
{filtered.map((path) => {
return (
<a
key={path.title}
href={path.link}
className="rounded-md border border-solid border-gray-200 p-6 !text-inherit no-underline hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
>
{path.icon && (
<div className="w-fit rounded-full bg-orange-50 p-1 text-orange-500 dark:bg-orange-950">
<svg
{...path.icon.attributes}
width={24}
height={24}
dangerouslySetInnerHTML={{ __html: path.icon.body }}
/>
</div>
)}
<p className="!mt-3 font-semibold">{path.title}</p>
<Markdown
className="!mt-1 text-sm leading-6"
disallowedElements={["a"]}
unwrapDisallowed={true}
>
{path.description}
</Markdown>
</a>
);
})}
</div>
</div>
);
};

export default LearningPathCatalog;
2 changes: 1 addition & 1 deletion src/content/learning-paths/cybersafe.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href='https://www.cloudflare.com/lp/cybersafe-schools/'>Project Cybersafe Schools</a> 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"]
Expand Down
2 changes: 1 addition & 1 deletion src/icons/learning-paths.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/icons/notifications.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/icons/speed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions src/pages/changelog/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Expand All @@ -41,10 +41,9 @@ const props = {
<div class="!mt-0 sm:flex" data-products={productIds}>
<time
datetime={entry.data.date.toISOString()}
class="whitespace-nowrap leading-none sm:pr-4 sm:text-right"
class="whitespace-nowrap leading-6 sm:pr-4 sm:text-right"
>
<strong class="text-xs">{date}</strong>
<span class="text-xs sm:block">{time}</span>
</time>
<Steps>
<ol class="!mt-0 overflow-x-auto">
Expand Down
Loading