Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement search React documentation command #348 #349

Merged
merged 4 commits into from
Feb 6, 2024
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
83 changes: 76 additions & 7 deletions src/features/commands.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import fetch from "node-fetch";
import { ChannelType, EmbedType, Message, TextChannel } from "discord.js";
import { APIEmbed, ChannelType, EmbedType, Message, TextChannel } from "discord.js";
import cooldown from "./cooldown";
import { ChannelHandlers } from "../types";
import { isStaff } from "../helpers/discord";
import {
getReactDocsContent,
getReactDocsSearchKey,
} from "../helpers/react-docs";

export const EMBED_COLOR = 7506394;

Expand Down Expand Up @@ -233,7 +237,7 @@ Check out [this article on "Why and how to bind methods in your React component
{
title: "Lifting State Up",
type: EmbedType.Rich,
description: `Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.
description: `Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.

Learn more about lifting state in the [React.dev article about "Sharing State Between Components"](https://react.dev/learn/sharing-state-between-components).`,
color: EMBED_COLOR,
Expand Down Expand Up @@ -402,7 +406,7 @@ Here's an article explaining the difference between the two: https://goshakkk.na
name: "MDN",
url: "https://developer.mozilla.org",
icon_url:
"https://developer.mozilla.org/static/img/opengraph-logo.72382e605ce3.png",
"https://developer.mozilla.org/favicon-48x48.cbbd161b.png",
},
title,
description,
Expand All @@ -415,6 +419,55 @@ Here's an article explaining the difference between the two: https://goshakkk.na
fetchMsg.delete();
},
},
{
words: ["!react-docs", "!docs"],
help: "Allows you to search the React docs, usage: !docs useState",
category: "Web",
handleMessage: async (msg) => {
const [, search] = msg.content.split(" ");

const searchKey = getReactDocsSearchKey(search);

if (!searchKey) {
msg.channel.send({
embeds: generateReactDocsErrorEmbeds(search),
});
return;
}

const [fetchMsg, content] = await Promise.all([
msg.channel.send(`Looking up documentation for **'${search}'**...`),
getReactDocsContent(searchKey),
]);

if (!content) {
fetchMsg.edit({
embeds: generateReactDocsErrorEmbeds(search),
});
return;
}

await msg.channel.send({
embeds: [
{
title: `${searchKey}`,
type: EmbedType.Rich,
description: content,
color: EMBED_COLOR,
url: `https://react.dev/reference/${searchKey}`,
author: {
name: "React documentation",
icon_url:
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1150px-React-icon.svg.png",
url: "https://react.dev/",
},
},
],
});

fetchMsg.delete();
},
},
{
words: [`!appideas`],
help: `provides a link to the best curated app ideas for beginners to advanced devs`,
Expand Down Expand Up @@ -832,7 +885,7 @@ https://exploringjs.com/es6/ch_variables.html#_pitfall-const-does-not-make-the-v
title: "Acquiring a remote position",
type: EmbedType.Rich,
description: `
Below is a list of resources we commonly point to as an aid in a search for remote jobs.
Below is a list of resources we commonly point to as an aid in a search for remote jobs.

NOTE: If you are looking for your first job in the field or are earlier in your career, then getting a remote job at this stage is incredibly rare. We recommend prioritizing getting a job local to the area you are in or possibly moving to an area for work if options are limited where you are.

Expand Down Expand Up @@ -882,9 +935,9 @@ Remote work has the most competition, and thus is likely to be more difficult to
title: "The importance of keys when rendering lists in React",
type: EmbedType.Rich,
description: `
React depends on the use of stable and unique keys to identify items in a list so that it can perform correct and performant DOM updates.
React depends on the use of stable and unique keys to identify items in a list so that it can perform correct and performant DOM updates.

Keys are particularly important if the list can change over time. React will use the index in the array by default if no key is specified. You can use the index in the array if the list doesn't change and you don't have a stable and unique key available.
Keys are particularly important if the list can change over time. React will use the index in the array by default if no key is specified. You can use the index in the array if the list doesn't change and you don't have a stable and unique key available.

Please see these resources for more information:

Expand Down Expand Up @@ -946,7 +999,7 @@ _ _
name: "Meta Frameworks",
value: `
- [Next.js](https://nextjs.org/)
- [Remix](https://remix.run/)
- [Remix](https://remix.run/)
- [Astro](https://astro.build/)
- [SvelteKit](https://kit.svelte.dev/)
- [Nuxt](https://nuxtjs.org/)
Expand Down Expand Up @@ -1117,4 +1170,20 @@ const commands: ChannelHandlers = {
},
};

const generateReactDocsErrorEmbeds = (search: string): APIEmbed[] => {
return [
{
type: EmbedType.Rich,
description: `Could not find anything on React documentation for **'${search}'**`,
color: EMBED_COLOR,
author: {
name: "React documentation",
icon_url:
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1150px-React-icon.svg.png",
url: "https://react.dev/",
},
},
];
};

export default commands;
121 changes: 121 additions & 0 deletions src/helpers/react-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import fetch from "node-fetch";
import { gitHubToken } from "./env";

const LOOKUP_REGEX = /<Intro>\s*(.*?)\s*<\/Intro>/gs;
const LINK_REGEX = /\[([^\]]+)\]\((?!https?:\/\/)([^)]+)\)/g;

const BASE_URL =
"https://api.github.com/repos/reactjs/react.dev/contents/src/content/reference/";

export const getReactDocsContent = async (searchPath: string) => {
try {
const response = await fetch(`${BASE_URL}${searchPath}.md`, {
method: "GET",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${gitHubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
const json = await response.json();
const contentBase64 = json.content;
const decodedContent = Buffer.from(contentBase64, "base64").toString(
"utf8",
);
return processReactDocumentation(decodedContent);
} catch (error) {
console.error("Error:", error);
return null;
}
};

export const getReactDocsSearchKey = (search: string) => {
const normalizedSearch = search.toLowerCase();

return REACT_AVAILABLE_DOCS.find((key) => {
const keyParts = key.split("/");
const name = keyParts[keyParts.length - 1];
const namespace =
keyParts.length <= 2 ? key : `${keyParts[0]}/${keyParts[2]}`;

return (
namespace.toLowerCase() === normalizedSearch ||
name.toLowerCase() === normalizedSearch
);
});
};

const processReactDocumentation = (content: string) => {
const patchedContentLinks = content.replace(LINK_REGEX, (_, text, link) => {
return `[${text}](https://react.dev${link})`;
});

const matches = [...patchedContentLinks.matchAll(LOOKUP_REGEX)];

if (matches.length > 0) {
const [introContent] = matches.map(([, match]) => match.trim());
return introContent;
}

return null;
};

const REACT_AVAILABLE_DOCS = [
"react/cache",
"react/Children",
"react/cloneElement",
"react/Component",
"react/createContext",
"react/createElement",
"react/createFactory",
"react/createRef",
"react/experimental_taintObjectReference",
"react/experimental_taintUniqueValue",
"react/experimental_useEffectEvent",
"react/forwardRef",
"react/Fragment",
"react/isValidElement",
"react/lazy",
"react/legacy",
"react/memo",
"react/Profiler",
"react/PureComponent",
"react/startTransition",
"react/StrictMode",
"react/Suspense",
"react/use-client",
"react/use-server",
"react/use",
"react/useCallback",
"react/useContext",
"react/useDebugValue",
"react/useDeferredValue",
"react/useEffect",
"react/useId",
"react/useImperativeHandle",
"react/useInsertionEffect",
"react/useLayoutEffect",
"react/useMemo",
"react/useOptimistic",
"react/useReducer",
"react/useRef",
"react/useState",
"react/useSyncExternalStore",
"react/useTransition",
"react-dom/client/createRoot",
"react-dom/client/hydrateRoot",
"react-dom/hooks/useFormState",
"react-dom/hooks/useFormStatus",
"react-dom/server/renderToNodeStream",
"react-dom/server/renderToPipeableStream",
"react-dom/server/renderToReadableStream",
"react-dom/server/renderToStaticMarkup",
"react-dom/server/renderToStaticNodeStream",
"react-dom/server/renderToString",
"react-dom/unmountComponentAtNode",
"react-dom/hydrate",
"react-dom/render",
"react-dom/createPortal",
"react-dom/findDOMNode",
"react-dom/flushSync",
];
Loading