Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
78 changes: 78 additions & 0 deletions src/components/ReactSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Select, { type Props } from "react-select";
import type { ActionMeta, StylesConfig } from "react-select";
import { setSearchParams } from "~/util/url";

export type Option = {
label: string;
value: string;
};

export default function ReactSelect(props: Props & { urlParam?: string }) {
const selectStyles: StylesConfig = {
control: (base, state) => ({
...base,
backgroundColor: "var(--sl-color-gray-6)",
borderColor: state.isFocused
? "var(--sl-color-gray-3)"
: "var(--sl-color-gray-4)",
"&:hover": {
borderColor: "var(--sl-color-gray-3)",
},
boxShadow: state.isFocused ? "0 0 0 1px var(--sl-color-gray-3)" : "none",
}),
menu: (base) => ({
...base,
backgroundColor: "var(--sl-color-gray-6)",
borderColor: "var(--sl-color-gray-4)",
}),
option: (base, state) => ({
...base,
backgroundColor: state.isFocused
? "var(--sl-color-gray-5)"
: "var(--sl-color-gray-6)",
color: "var(--sl-color-gray-1)",
"&:active": {
backgroundColor: "var(--sl-color-gray-4)",
},
}),
singleValue: (base) => ({
...base,
color: "var(--sl-color-gray-1)",
}),
input: (base) => ({
...base,
color: "var(--sl-color-gray-1)",
}),
groupHeading: (base) => ({
...base,
color: "var(--sl-color-gray-3)",
}),
};

const onChangeHandler = (
option: Option | null,
actionMeta: ActionMeta<Option>,
) => {
props.onChange?.(option, actionMeta);

const params = new URLSearchParams(window.location.search);

if (option) {
params.set(props.urlParam || "filters", option.value);
} else {
params.delete(props.urlParam || "filters");
}

setSearchParams(params);
};

return (
<Select
{...props}
styles={selectStyles}
onChange={(val: unknown, meta: ActionMeta<unknown>) =>
onChangeHandler(val as Option | null, meta as ActionMeta<Option>)
}
/>
);
}
52 changes: 36 additions & 16 deletions src/components/ResourcesBySelector.astro
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
---
import { z } from "astro:schema";
import { getCollection } from "astro:content";
import { getCollection, type CollectionEntry } from "astro:content";
import ResourcesBySelectorReact from "./ResourcesBySelector";

type Props = z.infer<typeof props>;
type Frontmatter = keyof CollectionEntry<"docs">["data"];

const props = z.object({
tags: z.string().array().optional(),
types: z.string().array(),
products: z.string().array().optional(),
directory: z.string().optional(),
filterables: z.custom<Frontmatter>().array().optional(),
});

const { tags, types, products } = props.parse(Astro.props);
const { tags, types, products, directory, filterables } = props.parse(
Astro.props,
);

const resources = await getCollection("docs", ({ data }) => {
const resources = await getCollection("docs", ({ id, data }) => {
return (
types.includes(data.pcx_content_type ?? "") &&
(directory ? id.startsWith(directory) : true) &&
(tags ? data.tags?.some((v: string) => tags.includes(v)) : true) &&
(products ? data.products?.some((v: string) => products.includes(v)) : true)
);
});

const facets = resources.reduce(
(acc, page) => {
if (!filterables) return acc;

for (const filter of filterables) {
const val = page.data[filter];
if (val) {
if (Array.isArray(val) && val.every((v) => typeof v === "string")) {
acc[filter] = [...new Set([...(acc[filter] || []), ...val])];
} else if (typeof val === "string") {
acc[filter] = [...new Set([...(acc[filter] || []), val])];
}
}
}

return acc;
},
{} as Record<string, string[]>,
);
---

<ul>
{
resources.map((page) => {
const description = page.data.description;
return (
<li>
<!-- prettier-ignore -->
<a href={`/${page.id}/`}>{page.data.title}</a>{description && `: ${description}`}
</li>
);
})
}
</ul>
<ResourcesBySelectorReact
resources={resources}
facets={facets}
filters={filterables}
client:load
/>
92 changes: 92 additions & 0 deletions src/components/ResourcesBySelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect, useState } from "react";
import ReactSelect from "./ReactSelect";
import type { CollectionEntry } from "astro:content";

type Frontmatter = keyof CollectionEntry<"docs">["data"];

interface Props {
resources: CollectionEntry<"docs">[];
facets: Record<string, string[]>;
filters?: Frontmatter[];
}

export default function ResourcesBySelector({
resources,
facets,
filters,
}: Props) {
const [selectedFilter, setSelectedFilter] = useState<string | null>(null);

const handleFilterChange = (option: any) => {
setSelectedFilter(option?.value || null);
};

const options = Object.entries(facets).map(([key, values]) => ({
label: key,
options: values.map((v) => ({
value: v,
label: v,
})),
}));

const visibleResources = resources.filter((resource) => {
if (!selectedFilter || !filters) return true;

const filterableValues: string[] = [];
for (const filter of filters) {
const val = resource.data[filter];
if (val) {
if (Array.isArray(val) && val.every((v) => typeof v === "string")) {
filterableValues.push(...val);
} else if (typeof val === "string") {
filterableValues.push(val);
}
}
}

return filterableValues.includes(selectedFilter);
});

useEffect(() => {
const params = new URLSearchParams(window.location.search);
const value = params.get("filters");

if (value) {
setSelectedFilter(value);
}
}, []);

return (
<div>
{filters && (
<div className="not-content">
<ReactSelect
className="mt-2"
value={{ value: selectedFilter, label: selectedFilter }}
options={options}
onChange={handleFilterChange}
isClearable
placeholder="Filter resources..."
/>
</div>
)}

<div className="grid grid-cols-2 gap-4">
{visibleResources.map((page) => (
<a
key={page.id}
href={`/${page.id}/`}
className="flex flex-col gap-2 rounded-sm border border-solid border-gray-200 p-6 text-black no-underline hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
>
<p className="decoration-accent underline decoration-2 underline-offset-4">
{page.data.title}
</p>
<span className="line-clamp-3" title={page.data.description}>
{page.data.description}
</span>
</a>
))}
</div>
</div>
);
}
56 changes: 5 additions & 51 deletions src/components/changelog/ProductSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import type { CollectionEntry } from "astro:content";
import type { StylesConfig } from "react-select";
import Select from "react-select";
import ReactSelect, { type Option } from "../ReactSelect";
import { useEffect, useState } from "react";

interface Props {
products: CollectionEntry<"products">[];
groups: string[];
}

interface Option {
label?: string;
value: string;
}

export default function ProductSelect({ products, groups }: Props) {
const [selectedOption, setSelectedOption] = useState<Option>();

Expand Down Expand Up @@ -42,47 +36,6 @@ export default function ProductSelect({ products, groups }: Props) {
},
];

const selectStyles: StylesConfig<Option, false> = {
control: (base, state) => ({
...base,
backgroundColor: "var(--sl-color-gray-6)",
borderColor: state.isFocused
? "var(--sl-color-gray-3)"
: "var(--sl-color-gray-4)",
"&:hover": {
borderColor: "var(--sl-color-gray-3)",
},
boxShadow: state.isFocused ? "0 0 0 1px var(--sl-color-gray-3)" : "none",
}),
menu: (base) => ({
...base,
backgroundColor: "var(--sl-color-gray-6)",
borderColor: "var(--sl-color-gray-4)",
}),
option: (base, state) => ({
...base,
backgroundColor: state.isFocused
? "var(--sl-color-gray-5)"
: "var(--sl-color-gray-6)",
color: "var(--sl-color-gray-1)",
"&:active": {
backgroundColor: "var(--sl-color-gray-4)",
},
}),
singleValue: (base) => ({
...base,
color: "var(--sl-color-gray-1)",
}),
input: (base) => ({
...base,
color: "var(--sl-color-gray-1)",
}),
groupHeading: (base) => ({
...base,
color: "var(--sl-color-gray-3)",
}),
};

useEffect(() => {
const url = new URL(window.location.href);
const param = url.searchParams.get("product");
Expand All @@ -100,6 +53,7 @@ export default function ProductSelect({ products, groups }: Props) {

const handleChange = (option: Option | null) => {
if (!option) return;

setSelectedOption(option);

const event = new Event("change");
Expand All @@ -114,13 +68,13 @@ export default function ProductSelect({ products, groups }: Props) {
};

return (
<Select
<ReactSelect
id="changelogs-next-filter"
className="mt-2"
options={options}
value={selectedOption}
onChange={handleChange}
styles={selectStyles}
onChange={(e) => handleChange(e as Option | null)}
urlParam="product"
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,32 @@ The `ResourcesBySelector` component allows you to pull in documentation resource
import { ResourcesBySelector } from "~/components";

<ResourcesBySelector
tags={["AI"]}
types={["reference-architecture","design-guide","reference-architecture-diagram"]}
directory="workers/examples/"
types={["example"]}
filterables={["languages", "tags"]}
/>
```

### Inputs

- `directory` <Type text="string" />

The directory to search for resources in, relative to `src/content/docs/`. For example, for Workers tutorials, `directory="workers/tutorials/"`.

- `filterables` <Type text="string[]" />

An array of frontmatter properties to show in the frontend filter dropdown. For example, `filterables={["languages", "tags"]}` will allow users to filter based on each pages `languages` and `tags` frontmatter.

- `types` <Type text="string[]" />

An array of `pcx_content_type` values to filter by.
An array of `pcx_content_type` values to filter by. For example, `types={["example"]}`.

- `tags` <Type text="string[]" /> <MetaInfo text="optional" />

An array of `tags` values to filter by.
An array of `tags` values to filter by. For example, `tags={["AI"]}`.

To see a list of the available tags, and which pages are associated with them, refer to [this list](/style-guide/frontmatter/tags/).
To see a list of the available tags, and which pages are associated with them, refer to [this list](/style-guide/frontmatter/tags/).

- `products` <Type text="string[]" /> <MetaInfo text="optional" />

An array of `products` values to filter by.
An array of `products` values to filter by. For example, `products={["D1"]}`.
Loading