Skip to content
Closed
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
152 changes: 152 additions & 0 deletions src/components/TutorialsGrid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
import { z } from "astro:schema";
import {
getCollection,
getEntry,
reference,
type CollectionEntry,
} from "astro:content";
import type { ComponentProps } from "astro/types";
import { Badge } from "@astrojs/starlight/components";

type Props = z.infer<typeof props>;
type BadgeType = ComponentProps<typeof Badge>;

const props = z.object({
product: reference("products").optional(),
frontmatterBadges: z.string().array().optional(),
});

let { product, frontmatterBadges } = props.parse(Astro.props);

type DocsEntry = CollectionEntry<"docs">;
type VideoEntry = CollectionEntry<"videos">;

const tutorials: Array<DocsEntry | VideoEntry> = [];

if (!product) {
product = {
id: Astro.url.pathname.split("/").filter(Boolean)[0],
collection: "products",
};
}

const productEntry = await getEntry(product);

if (!productEntry) {
throw new Error(
`[TutorialsGrid] Unable to find product YAML for ${product.id}.`,
);
}

const productTitle = productEntry.data.product.title;

const docs = await getCollection("docs", (entry) => {
return (
// pcx_content_type: tutorial
entry.data.pcx_content_type === "tutorial" &&
// /src/content/r2/**/*.mdx
(entry.id.startsWith(`${productEntry.id}/`) ||
// products: [R2]
entry.data.products?.some(
(v: string) => v.toUpperCase() === productTitle.toUpperCase(),
))
);
});

tutorials.push(...docs);

const videos = await getCollection("videos", (entry) => {
return entry.data.products.some(
(v: string) => v.toUpperCase() === productTitle.toUpperCase(),
);
});

tutorials.push(...videos);

tutorials.sort((a, b) => Number(b.data.updated) - Number(a.data.updated));

const difficulties = {
Beginner: "success",
Intermediate: "caution",
Advanced: "danger",
} as Record<string, ComponentProps<typeof Badge>["variant"]>;
---

<div class="grid grid-cols-2 gap-4">
{
tutorials.map((tutorial) => {
const title =
tutorial.collection === "docs" ? tutorial.data.title : tutorial.id;

const href =
tutorial.collection === "docs"
? `/${tutorial.id}/`
: tutorial.data.link;

const difficulty = tutorial.data.difficulty;

const YOUTUBE_DOMAINS = ["www.youtube.com", "youtube.com", "youtu.be"];
const youtube =
tutorial.collection === "videos" &&
YOUTUBE_DOMAINS.includes(new URL(tutorial.data.link).hostname);

const spotlight =
tutorial.collection === "docs" && tutorial.data.spotlight;

const badges: BadgeType[] = [];

if (difficulty) {
badges.push({
text: difficulty,
variant: difficulties[difficulty],
});
}

if (youtube) {
badges.push({
text: "YouTube",
variant: "note",
});
}

if (spotlight) {
badges.push({
text: "Community",
variant: "tip",
});
}

if (frontmatterBadges) {
for (const frontmatterBadge of frontmatterBadges) {
const values =
tutorial.data[frontmatterBadge as keyof typeof tutorial.data];

if (values) {
if (Array.isArray(values)) {
for (const value of values) {
badges.push({ text: value.toString() });
}
} else {
badges.push({ text: values.toString() });
}
}
}
}

return (
<a
href={href}
class="flex flex-col rounded border border-cl1-gray-8 p-6 !text-black no-underline hover:bg-cl1-gray-9 dark:border-cl1-gray-2 dark:bg-cl1-gray-0 dark:hover:bg-cl1-gray-1"
>
<strong>{title}</strong>
<p class="!mt-auto">
{badges.map((badge) => (
<Badge {...badge} />
))}
</p>
</a>
);
})
}
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on the look / design based on previous things I've heard and experienced.

  • Any way we're able to more clearly indicate which is an external resource vs hosted in our docs? Specific case for videos b/c some are hosted on YouTube. I suppose we could also solve this another way, i.e., centralizing video files somewhere and creating standalone pages for each Stream / YouTube entry.
  • Is there a better way to organize the pills so the info is less jumbled? I find myself sorta glazing over all of them b/c it's hard to pick out which one applies to which situation.

1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export { default as SpotlightAuthorDetails } from "./SpotlightAuthorDetails.astr
export { default as Stream } from "./Stream.astro";
export { default as TroubleshootingList } from "./TroubleshootingList.astro";
export { default as TunnelCalculator } from "./TunnelCalculator.astro";
export { default as TutorialsGrid } from "./TutorialsGrid.astro";
export { default as Type } from "./Type.astro";
export { default as TypeScriptExample } from "./TypeScriptExample.astro";
export { default as WranglerConfig } from "./WranglerConfig.astro";
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/ai-gateway/tutorials/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ sidebar:
hideIndex: true
---

import { GlossaryTooltip, ListTutorials } from "~/components";
import { GlossaryTooltip, TutorialsGrid } from "~/components";

View <GlossaryTooltip term="tutorial">tutorials</GlossaryTooltip> to help you get started with AI Gateway.

<ListTutorials />
<TutorialsGrid frontmatterBadges={["languages"]} />
45 changes: 45 additions & 0 deletions src/content/docs/style-guide/components/tutorials-grid.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Tutorials grid
---

This component fetches all pages with `pcx_content_type: tutorial` under a given product.

## Import

```mdx
import { TutorialsGrid } from "~/components";
```

## Usage

```mdx live
import { TutorialsGrid } from "~/components";

<TutorialsGrid product="workers" frontmatterBadges={["languages"]} />
```

## `<TutorialsGrid>` Props

### `product`

**type:** `string`

The product to fetch tutorials for, based on the file location and the `products` frontmatter property.

This property is optional, and will be inferred from the page it is used on if omitted.

### `frontmatterBadges`

**type:** `string[]`

Frontmatter values to add badges from.

For example, given the below frontmatter, the badges `foo` and `bar` would be added when `frontmatterBadges={["stuff"]}` is provided.

```mdx
---
title: Example Title
stuff:
- foo
- bar
```
4 changes: 2 additions & 2 deletions src/content/docs/workers-ai/tutorials/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ sidebar:
order: 5
---

import { GlossaryTooltip, ListTutorials } from "~/components";
import { GlossaryTooltip, TutorialsGrid } from "~/components";

:::note
[Explore our community-written tutorials contributed through the Developer Spotlight program.](/developer-spotlight/)
:::

View <GlossaryTooltip term="tutorial">tutorials</GlossaryTooltip> to help you get started with Workers AI.

<ListTutorials />
<TutorialsGrid frontmatterBadges={["languages"]} />
2 changes: 1 addition & 1 deletion src/content/docs/workers/tutorials/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ import { GlossaryTooltip, ListTutorials } from "~/components";

View <GlossaryTooltip term="tutorial">tutorials</GlossaryTooltip> to help you get started with Workers.

<ListTutorials />
<ListTutorials />
Loading