chore: move website and guide out of packages

This commit is contained in:
iCrawl
2022-10-10 01:22:48 +02:00
parent 0a9d57b011
commit 3ed668e539
175 changed files with 428 additions and 487 deletions

View File

@@ -0,0 +1 @@
NEXT_PUBLIC_LOCAL_DEV=true

View File

@@ -0,0 +1,12 @@
{
"extends": ["../../.eslintrc.json", "neon/react", "neon/next", "neon/edge", "neon/prettier"],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"react/react-in-jsx-scope": 0,
"react/jsx-filename-extension": [1, { "extensions": [".tsx"] }]
}
}

30
apps/website/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Packages
node_modules/
# Log files
logs/
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Env
.env
# Dist
dist/
typings/
.cache/
build/
api/
src/styles/unocss.css
.next/
# Miscellaneous
.tmp/
coverage/
.vercel
public/searchIndex

View File

@@ -0,0 +1 @@
module.exports = require('../../.lintstagedrc.json');

View File

@@ -0,0 +1,15 @@
# Autogenerated
CHANGELOG.md
.turbo
dist/
docs/**/*
!docs/index.yml
!docs/README.md
coverage/
.cache
build/
src/styles/unocss.css
api/
.next/
.vercel/
.cache/

View File

@@ -0,0 +1 @@
module.exports = require('../../.prettierrc.json');

190
apps/website/LICENSE Normal file
View File

@@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2022 Noel Buechler
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

46
apps/website/README.md Normal file
View File

@@ -0,0 +1,46 @@
<div align="center">
<br />
<p>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
</p>
<br />
<p>
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Build status" /></a>
</p>
<p>
<a href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-vercel.svg" alt="Vercel" /></a>
</p>
</div>
## Links
- [Website][website] ([source][website-source])
- [Documentation][documentation]
- [Guide][guide] ([source][guide-source])
See also the [Update Guide][guide-update], including updated and removed items in the library.
- [discord.js Discord server][discord]
- [Discord API Discord server][discord-api]
- [GitHub][source]
- [Related libraries][related-libs]
## Contributing
See [the contribution guide][contributing] if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord].
[website]: https://discord.js.org/
[website-source]: https://github.com/discordjs/discord.js/tree/main/apps/website
[documentation]: https://discord.js.org/
[guide]: https://discordjs.guide/
[guide-source]: https://github.com/discordjs/guide
[guide-update]: https://discordjs.guide/additional-info/changes-in-v14.html
[discord]: https://discord.gg/djs
[discord-api]: https://discord.gg/discord-api
[source]: https://github.com/discordjs/discord.js/tree/main/apps/website
[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries
[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md

5
apps/website/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -0,0 +1,22 @@
/* eslint-disable tsdoc/syntax */
import { URL, fileURLToPath } from 'node:url';
/**
* @type {import('next').NextConfig}
*/
export default {
reactStrictMode: true,
swcMinify: true,
eslint: {
ignoreDuringBuilds: true,
},
cleanDistDir: true,
experimental: {
outputFileTracingRoot: fileURLToPath(new URL('../../', import.meta.url)),
fallbackNodePolyfills: false,
},
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
};

97
apps/website/package.json Normal file
View File

@@ -0,0 +1,97 @@
{
"name": "@discordjs/website",
"version": "0.1.0",
"description": "Imagine a bot... the most popular way to build discord bots",
"private": true,
"scripts": {
"test": "vitest run",
"build:local": "yarn run --top-level docs --force && cross-env NEXT_PUBLIC_LOCAL_DEV=true yarn build:prod",
"build:prod": "yarn workspace @discordjs/api-extractor-utils run build && yarn workspace @discordjs/scripts run build && yarn workspace @discordjs/ui run build && yarn build:css && yarn build:next",
"build:next": "next build",
"build:css": "yarn generate:css",
"build:search_indicies": "yarn node scripts/generateAllIndicies.js",
"dev": "yarn run --top-level docs && concurrently 'yarn dev:css' 'yarn dev:next'",
"dev:next": "next dev",
"dev:css": "yarn generate:css --watch",
"generate:css": "unocss 'src/**/*.tsx' '../../packages/ui/src/lib/components/**/*.tsx' --out-file ./src/styles/unocss.css --config ../../packages/ui/unocss.config.ts",
"lint": "prettier --check . && cross-env TIMING=1 eslint src --ext .mjs,.js,.cjs,.ts,.tsx",
"format": "prettier --write . && cross-env TIMING=1 eslint src --ext .mjs,.js,.cjs,.ts,.tsx --fix"
},
"type": "module",
"contributors": [
"Crawl <icrawltogo@gmail.com>"
],
"license": "Apache-2.0",
"keywords": [
"discord",
"api",
"bot",
"client",
"node",
"discordapp",
"discordjs"
],
"repository": {
"type": "git",
"url": "https://github.com/discordjs/discord.js.git"
},
"bugs": {
"url": "https://github.com/discordjs/discord.js/issues"
},
"homepage": "https://discord.js.org",
"dependencies": {
"@discordjs/api-extractor-utils": "workspace:^",
"@discordjs/scripts": "workspace:^",
"@discordjs/ui": "workspace:^",
"@microsoft/api-extractor-model": "7.24.0",
"@microsoft/tsdoc": "0.14.1",
"@vscode/codicons": "^0.0.32",
"ariakit": "^2.0.0-next.41",
"cmdk": "^0.1.20",
"meilisearch": "^0.28.0",
"next": "^12.3.1",
"next-mdx-remote": "^4.1.0",
"next-progress": "^2.2.0",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-custom-scrollbars-2": "^4.5.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-syntax-highlighter": "^15.5.0",
"react-use": "^17.4.0",
"rehype-ignore": "^1.0.1",
"rehype-pretty-code": "^0.3.2",
"rehype-raw": "^6.1.1",
"rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.1",
"shiki": "^0.11.1",
"swr": "^1.3.0"
},
"devDependencies": {
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/node": "^16.11.64",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/react-syntax-highlighter": "^15.5.5",
"@unocss/cli": "^0.45.29",
"@unocss/reset": "^0.45.29",
"@vitejs/plugin-react": "^2.1.0",
"@vitest/coverage-c8": "^0.24.0",
"concurrently": "^7.4.0",
"cross-env": "^7.0.3",
"eslint": "^8.25.0",
"eslint-config-neon": "^0.1.37",
"happy-dom": "^7.4.0",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
"typescript": "^4.8.4",
"unocss": "^0.45.29",
"vercel": "^28.4.8",
"vitest": "^0.24.0"
},
"engines": {
"node": ">=16.9.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<wide310x150logo src="/mstile-310x150.png"/>
<TileColor>#090a16</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="400.000000pt" height="400.000000pt" viewBox="0 0 400.000000 400.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,400.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 2000 l0 -2000 2000 0 2000 0 0 2000 0 2000 -2000 0 -2000 0 0
-2000z m1305 795 c219 -47 373 -197 421 -411 24 -109 16 -315 -16 -399 l-23
-61 -59 -11 c-110 -21 -182 -92 -203 -202 -11 -56 -11 -56 -68 -79 -76 -30
-201 -42 -464 -42 l-223 0 0 610 0 610 283 0 c210 0 300 -4 352 -15z m890
-585 c0 -543 -2 -601 -18 -659 -56 -198 -190 -334 -365 -370 -176 -37 -349 5
-471 115 -48 44 -111 134 -111 161 0 6 19 14 43 18 23 4 73 18 110 32 l68 24
50 -45 c56 -50 98 -66 175 -66 100 0 207 81 234 178 6 23 10 251 10 625 l0
588 138 -3 137 -3 0 -595z m930 570 c93 -29 197 -84 252 -134 l44 -41 -61 -75
c-34 -41 -67 -81 -74 -88 -10 -10 -25 -4 -78 32 -95 63 -154 81 -268 81 -79 0
-103 -4 -142 -23 -62 -31 -90 -71 -96 -137 -8 -94 28 -137 168 -202 157 -73
172 -80 240 -108 236 -97 341 -215 356 -400 16 -193 -82 -363 -258 -449 -91
-45 -187 -66 -303 -66 -191 0 -390 74 -524 195 l-40 37 80 93 80 94 73 -53
c168 -121 353 -155 491 -92 72 33 108 82 113 155 9 122 -37 162 -323 286 -258
111 -350 179 -407 300 -69 148 -37 343 75 463 61 65 195 133 300 152 79 14
222 4 302 -20z m-1367 -977 c45 -34 65 -69 70 -125 4 -61 -19 -106 -77 -145
-90 -60 -226 3 -238 111 -3 24 -3 56 1 70 8 34 54 86 91 102 40 19 120 12 153
-13z"/>
<path d="M967 2543 c-4 -3 -7 -159 -7 -346 l0 -339 128 4 c101 3 136 8 172 24
106 48 162 145 168 292 10 214 -69 334 -238 361 -73 12 -213 15 -223 4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "discord.js",
"short_name": "discord.js",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#1a1b1e",
"background_color": "#1a1b1e",
"display": "standalone"
}

View File

@@ -0,0 +1,5 @@
import { generateAllIndicies } from '@discordjs/scripts';
console.log('Generating all indicies...');
await generateAllIndicies();
console.log('Generated all indicies.');

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,136 @@
import type { ApiItemKind } from '@microsoft/api-extractor-model';
import { Dialog } from 'ariakit/dialog';
import { Command } from 'cmdk';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import {
VscArrowRight,
VscSymbolClass,
VscSymbolEnum,
VscSymbolField,
VscSymbolInterface,
VscSymbolMethod,
VscSymbolProperty,
VscSymbolVariable,
} from 'react-icons/vsc';
import { useKey } from 'react-use';
import { useCmdK } from '~/contexts/cmdK';
import { client } from '~/util/search';
function resolveIcon(item: keyof ApiItemKind) {
switch (item) {
case 'Class':
return <VscSymbolClass className="shrink-0" size={25} />;
case 'Enum':
return <VscSymbolEnum className="shrink-0" size={25} />;
case 'Interface':
return <VscSymbolInterface className="shrink-0" size={25} />;
case 'Property':
return <VscSymbolProperty className="shrink-0" size={25} />;
case 'TypeAlias':
return <VscSymbolField className="shrink-0" size={25} />;
case 'Variables':
return <VscSymbolVariable className="shrink-0" size={25} />;
default:
return <VscSymbolMethod className="shrink-0" size={25} />;
}
}
export function CmdKDialog({
currentPackageName,
currentVersion,
}: {
currentPackageName?: string | undefined;
currentVersion?: string | undefined;
}) {
const router = useRouter();
const dialog = useCmdK();
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const searchResultItems = useMemo(
() =>
searchResults?.map((item) => (
<Command.Item
className="dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 [&[aria-selected]]:ring-blurple [&[aria-selected]]:ring-width-4 [&[aria-selected]]:ring my-1 flex flex transform-gpu cursor-pointer select-none appearance-none flex-col place-content-center rounded bg-transparent px-4 py-2 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 active:translate-y-px active:bg-neutral-200 dark:text-white"
key={item.id}
onSelect={() => {
void router.push(item.path);
dialog!.setOpen(false);
}}
>
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex flex-row place-items-center gap-4">
{resolveIcon(item.kind)}
<div className="w-50 sm:w-100 flex flex-col">
<h2 className="font-semibold">{item.name}</h2>
<div className="line-clamp-1 text-sm font-normal">{item.summary}</div>
<div className="line-clamp-1 hidden text-xs font-light opacity-75 dark:opacity-50 sm:block">
{item.path}
</div>
</div>
</div>
<VscArrowRight className="shrink-0" size={20} />
</div>
</Command.Item>
)) ?? [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchResults],
);
useKey(
(event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
return true;
}
return false;
},
dialog!.toggle,
{ event: 'keydown', options: {} },
[],
);
useEffect(() => {
if (!dialog!.open) {
setSearch('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialog!.open]);
useEffect(() => {
const searchDoc = async (searchString: string, version: string) => {
const res = await client.index(`${currentPackageName}-${version}`).search(searchString, { limit: 5 });
setSearchResults(res.hits);
};
if (search && currentPackageName) {
void searchDoc(search, currentVersion?.replaceAll('.', '-') ?? 'main');
} else {
setSearchResults([]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search]);
return (
<Dialog className="fixed top-1/4 left-1/2 z-50 -translate-x-1/2" state={dialog!}>
<Command
className="dark:bg-dark-300 min-w-xs sm:min-w-lg max-w-xs rounded bg-white sm:max-w-lg"
label="Command Menu"
shouldFilter={false}
>
<Command.Input
className="dark:bg-dark-300 caret-blurple placeholder:text-dark-300/75 focus:ring-width-2 focus:ring-blurple w-full rounded border-0 bg-white p-4 text-lg outline-0 outline-0 focus:ring dark:placeholder:text-white/75"
onValueChange={setSearch}
placeholder="Quick search..."
value={search}
/>
<Command.List className="pt-0">
<Command.Empty className="p-4 text-center">No results found</Command.Empty>
{search ? searchResultItems : null}
</Command.List>
</Command>
</Dialog>
);
}

View File

@@ -0,0 +1,86 @@
import type { TokenDocumentation, ApiItemJSON, AnyDocNodeJSON, InheritanceData } from '@discordjs/api-extractor-utils';
import type { PropsWithChildren } from 'react';
import { FiLink } from 'react-icons/fi';
import { HyperlinkedText } from './HyperlinkedText';
import { InheritanceText } from './InheritanceText';
import { TSDoc } from './tsdoc/TSDoc';
export enum CodeListingSeparatorType {
Type = ':',
Value = '=',
}
export function CodeListing({
name,
separator = CodeListingSeparatorType.Type,
typeTokens,
readonly = false,
optional = false,
summary,
children,
comment,
deprecation,
inheritanceData,
}: PropsWithChildren<{
comment?: AnyDocNodeJSON | null;
deprecation?: AnyDocNodeJSON | null;
inheritanceData?: InheritanceData | null;
name: string;
optional?: boolean;
readonly?: boolean;
separator?: CodeListingSeparatorType;
summary?: ApiItemJSON['summary'];
typeTokens: TokenDocumentation[];
}>) {
return (
<div className="scroll-mt-30 flex flex-col gap-4" id={name}>
<div className="md:-ml-8.5 flex flex-col gap-0.5 md:flex-row md:place-items-center md:gap-2">
<a
aria-label="Anchor"
className="focus:ring-width-2 focus:ring-blurple hidden rounded outline-0 focus:ring md:inline-block"
href={`#${name}`}
>
<FiLink size={20} />
</a>
{deprecation || readonly || optional ? (
<div className="flex flex-row gap-1">
{deprecation ? (
<div className="flex h-5 place-content-center place-items-center rounded-full bg-red-500 px-3 text-center text-xs font-semibold uppercase text-white">
Deprecated
</div>
) : null}
{readonly ? (
<div className="bg-blurple flex h-5 place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Readonly
</div>
) : null}
{optional ? (
<div className="bg-blurple flex h-5 place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Optional
</div>
) : null}
</div>
) : null}
<div className="flex flex-row flex-wrap place-items-center gap-1">
<h4 className="break-all font-mono text-lg font-bold">
{name}
{optional ? '?' : ''}
</h4>
<h4 className="font-mono text-lg font-bold">{separator}</h4>
<h4 className="break-all font-mono text-lg font-bold">
<HyperlinkedText tokens={typeTokens} />
</h4>
</div>
</div>
{summary || inheritanceData ? (
<div className="flex flex-col gap-4">
{deprecation ? <TSDoc node={deprecation} /> : null}
{summary ? <TSDoc node={summary} /> : null}
{comment ? <TSDoc node={comment} /> : null}
{inheritanceData ? <InheritanceText data={inheritanceData} /> : null}
{children}
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,135 @@
import type {
ApiItemJSON,
TokenDocumentation,
TypeParameterData,
ApiClassJSON,
ApiInterfaceJSON,
} from '@discordjs/api-extractor-utils';
import { Section } from '@discordjs/ui';
import type { ReactNode } from 'react';
import { Fragment, type PropsWithChildren } from 'react';
import { Scrollbars } from 'react-custom-scrollbars-2';
import {
VscSymbolClass,
VscSymbolMethod,
VscSymbolEnum,
VscSymbolInterface,
VscSymbolVariable,
VscListSelection,
VscSymbolParameter,
} from 'react-icons/vsc';
import { useMedia } from 'react-use';
import { HyperlinkedText } from './HyperlinkedText';
import { SyntaxHighlighter } from './SyntaxHighlighter';
import { TableOfContentItems } from './TableOfContentItems';
import { TypeParamTable } from './TypeParamTable';
import { TSDoc } from './tsdoc/TSDoc';
type DocContainerProps = PropsWithChildren<{
excerpt: string;
extendsTokens?: TokenDocumentation[] | null;
implementsTokens?: TokenDocumentation[][];
kind: string;
methods?: ApiClassJSON['methods'] | ApiInterfaceJSON['methods'] | null;
name: string;
properties?: ApiClassJSON['properties'] | ApiInterfaceJSON['properties'] | null;
subHeading?: ReactNode;
summary?: ApiItemJSON['summary'];
typeParams?: TypeParameterData[];
}>;
function generateIcon(kind: string) {
const icons = {
Class: <VscSymbolClass />,
Method: <VscSymbolMethod />,
Function: <VscSymbolMethod />,
Enum: <VscSymbolEnum />,
Interface: <VscSymbolInterface />,
TypeAlias: <VscSymbolVariable />,
};
return icons[kind as keyof typeof icons];
}
export function DocContainer({
name,
kind,
excerpt,
summary,
typeParams,
children,
extendsTokens,
implementsTokens,
methods,
properties,
subHeading,
}: DocContainerProps) {
const matches = useMedia('(max-width: 768px)', true);
return (
<>
<div className="flex flex-col gap-4">
<h2 className="flex flex-row place-items-center gap-2 break-all text-2xl font-bold">
<span>{generateIcon(kind)}</span>
{name}
</h2>
{subHeading}
<Section dense={matches} icon={<VscListSelection size={20} />} padded title="Summary">
{summary ? <TSDoc node={summary} /> : <span>No summary provided.</span>}
<div className="border-light-900 dark:border-dark-100 -mx-8 mt-6 border-t-2" />
</Section>
<SyntaxHighlighter code={excerpt} />
{extendsTokens?.length ? (
<div className="flex flex-row place-items-center gap-4">
<h3 className="text-xl font-bold">Extends</h3>
<span className="break-all font-mono">
<HyperlinkedText tokens={extendsTokens} />
</span>
</div>
) : null}
{implementsTokens?.length ? (
<div className="flex flex-row place-items-center gap-4">
<h3 className="text-xl font-bold">Implements</h3>
<span className="break-all font-mono">
{implementsTokens.map((token, idx) => (
<Fragment key={idx}>
<HyperlinkedText tokens={token} />
{idx < implementsTokens.length - 1 ? ', ' : ''}
</Fragment>
))}
</span>
</div>
) : null}
<div className="flex flex-col gap-4">
{typeParams?.length ? (
<Section
defaultClosed
dense={matches}
icon={<VscSymbolParameter size={20} />}
padded
title="Type Parameters"
>
<TypeParamTable data={typeParams} />
</Section>
) : null}
{children}
</div>
</div>
{(kind === 'Class' || kind === 'Interface') && (methods?.length || properties?.length) ? (
<aside className="dark:bg-dark-600 dark:border-dark-100 border-light-800 fixed top-[72px] right-0 bottom-0 z-20 hidden h-[calc(100vh_-_72px)] w-64 border-l bg-white pr-2 xl:block">
<Scrollbars
autoHide
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="dark:bg-dark-100 bg-light-900 z-30 rounded" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute top-0.5 right-0.5 bottom-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<TableOfContentItems methods={methods ?? []} properties={properties ?? []} />
</Scrollbars>
</aside>
) : null}
</>
);
}

View File

@@ -0,0 +1,22 @@
import type { TokenDocumentation } from '@discordjs/api-extractor-utils';
import Link from 'next/link';
export function HyperlinkedText({ tokens }: { tokens: TokenDocumentation[] }) {
return (
<>
{tokens.map((token, idx) => {
if (token.path) {
return (
<Link href={token.path} key={idx} prefetch={false}>
<a className="text-blurple focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring">
{token.text}
</a>
</Link>
);
}
return <span key={idx}>{token.text}</span>;
})}
</>
);
}

View File

@@ -0,0 +1,15 @@
import type { InheritanceData } from '@discordjs/api-extractor-utils';
import Link from 'next/link';
export function InheritanceText({ data }: { data: InheritanceData }) {
return (
<span className="font-semibold">
Inherited from{' '}
<Link href={data.path} prefetch={false}>
<a className="text-blurple focus:ring-width-2 focus:ring-blurple rounded font-mono outline-0 focus:ring">
{data.parentName}
</a>
</Link>
</span>
);
}

View File

@@ -0,0 +1,121 @@
import type { ApiMethodJSON, ApiMethodSignatureJSON } from '@discordjs/api-extractor-utils';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit';
import { useCallback, useMemo, useState } from 'react';
import { FiLink } from 'react-icons/fi';
import { VscChevronDown, VscVersions } from 'react-icons/vsc';
import { HyperlinkedText } from './HyperlinkedText';
import { InheritanceText } from './InheritanceText';
import { ParameterTable } from './ParameterTable';
import { TSDoc } from './tsdoc/TSDoc';
export function MethodItem({ data }: { data: ApiMethodJSON | ApiMethodSignatureJSON }) {
const method = data as ApiMethodJSON;
const [overloadIndex, setOverloadIndex] = useState(1);
const overloadedData = method.mergedSiblings[overloadIndex - 1]!;
const menu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
const key = useMemo(
() => `${data.name}${data.overloadIndex && data.overloadIndex > 1 ? `:${data.overloadIndex}` : ''}`,
[data.name, data.overloadIndex],
);
const getShorthandName = useCallback(
(data: ApiMethodJSON | ApiMethodSignatureJSON) =>
`${data.name}${data.optional ? '?' : ''}(${data.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`,
[],
);
return (
<div className="scroll-mt-30 flex flex-col gap-4" id={key}>
<div className="flex flex-col">
<div className="flex flex-col gap-2 md:-ml-9 md:flex-row md:place-items-center">
<a
aria-label="Anchor"
className="focus:ring-width-2 focus:ring-blurple hidden rounded outline-0 focus:ring md:inline-block"
href={`#${key}`}
>
<FiLink size={20} />
</a>
{data.deprecated ||
(data.kind === 'Method' && method.protected) ||
(data.kind === 'Method' && method.static) ? (
<div className="flex flex-row gap-1">
{data.deprecated ? (
<div className="flex h-5 place-content-center place-items-center rounded-full bg-red-500 px-3 text-center text-xs font-semibold uppercase text-white">
Deprecated
</div>
) : null}
{data.kind === 'Method' && method.protected ? (
<div className="bg-blurple flex h-5 place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Protected
</div>
) : null}
{data.kind === 'Method' && method.static ? (
<div className="bg-blurple flex h-5 place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Static
</div>
) : null}
</div>
) : null}
<div className="flex flex-row flex-wrap gap-1">
<h4 className="break-all font-mono text-lg font-bold">{getShorthandName(overloadedData)}</h4>
<h4 className="font-mono text-lg font-bold">:</h4>
<h4 className="break-all font-mono text-lg font-bold">
<HyperlinkedText tokens={data.returnTypeTokens} />
</h4>
</div>
</div>
</div>
{data.mergedSiblings.length > 1 ? (
<div className="flex flex-row place-items-center gap-2">
<MenuButton
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-0 focus:ring"
state={menu}
>
<div className="flex flex-row place-content-between place-items-center gap-2">
<VscVersions size={20} />
<div>
<span className="font-semibold">{`Overload ${overloadIndex}`}</span>
{` of ${data.mergedSiblings.length}`}
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${menu.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="dark:bg-dark-600 border-light-800 dark:border-dark-100 focus:ring-width-2 focus:ring-blurple z-20 flex flex-col rounded border bg-white p-1 outline-0 focus:ring"
state={menu}
>
{data.mergedSiblings.map((_, idx) => (
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 cursor-pointer rounded bg-white p-3 text-sm outline-0 focus:ring"
key={idx}
onClick={() => setOverloadIndex(idx + 1)}
>
{`Overload ${idx + 1}`}
</MenuItem>
))}
</Menu>
</div>
) : null}
{data.summary || data.parameters.length ? (
<div className="mb-4 flex flex-col gap-4">
{overloadedData.deprecated ? <TSDoc node={overloadedData.deprecated} /> : null}
{overloadedData.summary ?? data.summary ? <TSDoc node={overloadedData.summary ?? data.summary!} /> : null}
{overloadedData.remarks ? <TSDoc node={overloadedData.remarks} /> : null}
{overloadedData.comment ? <TSDoc node={overloadedData.comment} /> : null}
{overloadedData.parameters.length ? <ParameterTable data={overloadedData.parameters} /> : null}
{data.inheritanceData ? <InheritanceText data={data.inheritanceData} /> : null}
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import type { ApiMethodJSON, ApiMethodSignatureJSON } from '@discordjs/api-extractor-utils';
import { Fragment, useMemo } from 'react';
import { MethodItem } from './MethodItem';
export function MethodList({ data }: { data: (ApiMethodJSON | ApiMethodSignatureJSON)[] }) {
const methodItems = useMemo(
() =>
data
.filter((method) => method.overloadIndex <= 1)
.map((method) => (
<Fragment
key={`${method.name}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`}
>
<MethodItem data={method} />
<div className="border-light-900 dark:border-dark-100 -mx-8 border-t-2" />
</Fragment>
)),
[data],
);
return <div className="flex flex-col gap-4">{methodItems}</div>;
}

View File

@@ -0,0 +1,29 @@
import type { ParameterDocumentation } from '@discordjs/api-extractor-utils';
import { useMemo } from 'react';
import { HyperlinkedText } from './HyperlinkedText';
import { Table } from './Table';
import { TSDoc } from './tsdoc/TSDoc';
const columnStyles = {
Name: 'font-mono whitespace-nowrap',
Type: 'font-mono whitespace-pre-wrap break-normal',
};
export function ParameterTable({ data }: { data: ParameterDocumentation[] }) {
const rows = useMemo(
() =>
data.map((param) => ({
Name: param.name,
Type: <HyperlinkedText tokens={param.tokens} />,
Optional: param.isOptional ? 'Yes' : 'No',
Description: param.paramCommentBlock ? <TSDoc node={param.paramCommentBlock} /> : 'None',
})),
[data],
);
return (
<div className="overflow-x-auto">
<Table columnStyles={columnStyles} columns={['Name', 'Type', 'Optional', 'Description']} rows={rows} />
</div>
);
}

View File

@@ -0,0 +1,27 @@
import type { ApiPropertyItemJSON } from '@discordjs/api-extractor-utils';
import { Fragment, useMemo } from 'react';
import { CodeListing } from './CodeListing';
export function PropertyList({ data }: { data: ApiPropertyItemJSON[] }) {
const propertyItems = useMemo(
() =>
data.map((prop) => (
<Fragment key={prop.name}>
<CodeListing
comment={prop.comment}
deprecation={prop.deprecated}
inheritanceData={prop.inheritanceData}
name={prop.name}
optional={prop.optional}
readonly={prop.readonly}
summary={prop.summary}
typeTokens={prop.propertyTypeTokens}
/>
<div className="border-light-900 dark:border-dark-100 -mx-8 border-t-2" />
</Fragment>
)),
[data],
);
return <div className="flex flex-col gap-4">{propertyItems}</div>;
}

View File

@@ -0,0 +1,95 @@
import type {
ApiClassJSON,
ApiInterfaceJSON,
ParameterDocumentation,
ApiConstructorJSON,
} from '@discordjs/api-extractor-utils';
import { Section } from '@discordjs/ui';
import { useMemo } from 'react';
import { VscSymbolConstant, VscSymbolMethod, VscSymbolProperty } from 'react-icons/vsc';
import { useMedia } from 'react-use';
import { MethodList } from './MethodList';
import { ParameterTable } from './ParameterTable';
import { PropertyList } from './PropertyList';
import { TSDoc } from './tsdoc/TSDoc';
export function PropertiesSection({ data }: { data: ApiClassJSON['properties'] | ApiInterfaceJSON['properties'] }) {
const matches = useMedia('(max-width: 768px)', true);
return data.length ? (
<Section dense={matches} icon={<VscSymbolProperty size={20} />} padded title="Properties">
<PropertyList data={data} />
</Section>
) : null;
}
export function MethodsSection({ data }: { data: ApiClassJSON['methods'] | ApiInterfaceJSON['methods'] }) {
const matches = useMedia('(max-width: 768px)', true);
return data.length ? (
<Section dense={matches} icon={<VscSymbolMethod size={20} />} padded title="Methods">
<MethodList data={data} />
</Section>
) : null;
}
export function ParametersSection({ data }: { data: ParameterDocumentation[] }) {
const matches = useMedia('(max-width: 768px)', true);
return data.length ? (
<Section dense={matches} icon={<VscSymbolConstant size={20} />} padded title="Parameters">
<ParameterTable data={data} />
</Section>
) : null;
}
export function ConstructorSection({ data }: { data: ApiConstructorJSON }) {
const matches = useMedia('(max-width: 768px)', true);
const getShorthandName = useMemo(
() =>
`constructor(${data.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`,
[data.parameters],
);
return data.parameters.length ? (
<Section dense={matches} icon={<VscSymbolMethod size={20} />} padded title="Constructor">
<div className="scroll-mt-30 flex flex-col gap-4" id={data.name}>
<div className="flex flex-col">
<div className="flex flex-col gap-2 md:flex-row md:place-items-center">
{data.deprecated || data.protected ? (
<div className="flex flex-row gap-1">
{data.deprecated ? (
<div className="flex h-5 place-content-center place-items-center rounded-full bg-red-500 px-3 text-center text-xs font-semibold uppercase text-white">
Deprecated
</div>
) : null}
{data.protected ? (
<div className="bg-blurple flex h-5 place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Protected
</div>
) : null}
</div>
) : null}
<h4 className="break-all font-mono text-lg font-bold">{getShorthandName}</h4>
</div>
</div>
{data.summary || data.parameters.length ? (
<div className="mb-4 flex flex-col gap-4">
{data.deprecated ? <TSDoc node={data.deprecated} /> : null}
{data.summary ? <TSDoc node={data.summary} /> : null}
{data.remarks ? <TSDoc node={data.remarks} /> : null}
{data.comment ? <TSDoc node={data.comment} /> : null}
{data.parameters.length ? <ParameterTable data={data.parameters} /> : null}
</div>
) : null}
</div>
</Section>
) : null;
}

View File

@@ -0,0 +1,113 @@
import { Section } from '@discordjs/ui';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { type Dispatch, type SetStateAction, useEffect, useState, useMemo } from 'react';
import {
VscSymbolClass,
VscSymbolEnum,
VscSymbolInterface,
VscSymbolField,
VscSymbolVariable,
VscSymbolMethod,
} from 'react-icons/vsc';
import type { GroupedMembers, Members } from './SidebarLayout';
function groupMembers(members: Members): GroupedMembers {
const Classes: Members = [];
const Enums: Members = [];
const Interfaces: Members = [];
const Types: Members = [];
const Variables: Members = [];
const Functions: Members = [];
for (const member of members) {
switch (member.kind) {
case 'Class':
Classes.push(member);
break;
case 'Enum':
Enums.push(member);
break;
case 'Interface':
Interfaces.push(member);
break;
case 'TypeAlias':
Types.push(member);
break;
case 'Variable':
Variables.push(member);
break;
case 'Function':
Functions.push(member);
break;
default:
break;
}
}
return { Classes, Functions, Enums, Interfaces, Types, Variables };
}
function resolveIcon(item: keyof GroupedMembers) {
switch (item) {
case 'Classes':
return <VscSymbolClass size={20} />;
case 'Enums':
return <VscSymbolEnum size={20} />;
case 'Interfaces':
return <VscSymbolInterface size={20} />;
case 'Types':
return <VscSymbolField size={20} />;
case 'Variables':
return <VscSymbolVariable size={20} />;
default:
return <VscSymbolMethod size={20} />;
}
}
export function SidebarItems({
members,
setOpened,
}: {
members: Members;
setOpened: Dispatch<SetStateAction<boolean>>;
}) {
const router = useRouter();
const [asPathWithoutQueryAndAnchor, setAsPathWithoutQueryAndAnchor] = useState('');
const groupItems = useMemo(() => groupMembers(members), [members]);
useEffect(() => {
setAsPathWithoutQueryAndAnchor(router.asPath.split('?')[0]?.split('#')[0] ?? '');
}, [router.asPath]);
return (
<div className="flex flex-col gap-3 p-3 pb-32 lg:pb-12">
{(Object.keys(groupItems) as (keyof GroupedMembers)[])
.filter((group) => groupItems[group].length)
.map((group, idx) => (
<Section icon={resolveIcon(group)} key={idx} title={group}>
{groupItems[group].map((member, index) => (
<Link href={member.path} key={index} prefetch={false}>
<a
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l p-[5px] pl-6 outline-0 focus:rounded focus:border-0 focus:ring ${
asPathWithoutQueryAndAnchor === member.path
? 'bg-blurple text-white'
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'
}`}
onClick={() => setOpened(false)}
title={member.name}
>
<div className="flex flex-row place-items-center gap-2 lg:text-sm">
<span className="truncate">{member.name}</span>
{member.overloadIndex && member.overloadIndex > 1 ? (
<span className="text-xs">{member.overloadIndex}</span>
) : null}
</div>
</a>
</Link>
))}
</Section>
))}
</div>
);
}

View File

@@ -0,0 +1,378 @@
import type { getMembers, ApiItemJSON, ApiClassJSON, ApiInterfaceJSON } from '@discordjs/api-extractor-utils';
import { Button } from 'ariakit/button';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import Image from 'next/future/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { useTheme } from 'next-themes';
import { type PropsWithChildren, useState, useEffect, useMemo, Fragment } from 'react';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { FiCommand } from 'react-icons/fi';
import {
VscChevronDown,
VscColorMode,
VscGithubInverted,
VscMenu,
VscPackage,
VscSearch,
VscVersions,
} from 'react-icons/vsc';
import { useMedia /* useLockBodyScroll */ } from 'react-use';
import useSWR from 'swr';
import vercelLogo from '../assets/powered-by-vercel.svg';
import { CmdKDialog } from './CmdK';
import { SidebarItems } from './SidebarItems';
import { useCmdK } from '~/contexts/cmdK';
import { PACKAGES } from '~/util/constants';
import { fetcher } from '~/util/fetcher';
import type { findMember } from '~/util/model.server';
export interface SidebarLayoutProps {
branchName: string;
data: {
member: ReturnType<typeof findMember>;
members: ReturnType<typeof getMembers>;
searchIndex: any[];
source: MDXRemoteSerializeResult;
};
packageName: string;
selectedMember?: ApiItemJSON | undefined;
}
export type Members = SidebarLayoutProps['data']['members'];
export interface GroupedMembers {
Classes: Members;
Enums: Members;
Functions: Members;
Interfaces: Members;
Types: Members;
Variables: Members;
}
export function SidebarLayout({
packageName,
branchName,
data,
children,
}: PropsWithChildren<Partial<SidebarLayoutProps>>) {
const router = useRouter();
const dialog = useCmdK();
const [asPathWithoutQueryAndAnchor, setAsPathWithoutQueryAndAnchor] = useState('');
const { data: versions } = useSWR<string[]>(`https://docs.discordjs.dev/api/info?package=${packageName}`, fetcher);
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
const matches = useMedia('(min-width: 992px)', false);
const [opened, setOpened] = useState(false);
const packageMenu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
const versionMenu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
// useLockBodyScroll(opened);
useEffect(() => {
if (matches) {
setOpened(false);
}
}, [matches]);
useEffect(() => {
setAsPathWithoutQueryAndAnchor(router.asPath.split('?')[0]?.split('#')[0]?.split(':')[0] ?? '');
}, [router.asPath]);
const packageMenuItems = useMemo(
() => [
<a href="https://discord.js.org/#/docs/discord.js" key="discord.js">
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 rounded bg-white p-3 text-sm outline-0 focus:ring"
onClick={() => packageMenu.setOpen(false)}
state={packageMenu}
>
discord.js
</MenuItem>
</a>,
...PACKAGES.map((pkg) => (
<Link href={`/docs/packages/${pkg}/main`} key={pkg} passHref prefetch={false}>
<MenuItem
as="a"
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 rounded bg-white p-3 text-sm outline-0 focus:ring"
onClick={() => packageMenu.setOpen(false)}
state={packageMenu}
>
{pkg}
</MenuItem>
</Link>
)),
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const versionMenuItems = useMemo(
() =>
versions
?.map((item) => (
<Link href={`/docs/packages/${packageName}/${item}`} key={item} passHref prefetch={false}>
<MenuItem
as="a"
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 rounded bg-white p-3 text-sm outline-0 focus:ring"
onClick={() => versionMenu.setOpen(false)}
state={versionMenu}
>
{item}
</MenuItem>
</Link>
))
.reverse() ?? [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[versions, packageName],
);
const pathElements = useMemo(
() =>
asPathWithoutQueryAndAnchor
.split('/')
.slice(1)
.map((path, idx, original) => (
<Link href={`/${original.slice(0, idx + 1).join('/')}`} key={idx} prefetch={false}>
<a className="focus:ring-width-2 focus:ring-blurple rounded outline-0 hover:underline focus:ring">{path}</a>
</Link>
)),
[asPathWithoutQueryAndAnchor],
);
const breadcrumbs = useMemo(
() =>
pathElements.flatMap((el, idx, array) => {
if (idx === 0) {
return (
<Fragment key={idx}>
<div className="mx-2">/</div>
{el}
<div className="mx-2">/</div>
</Fragment>
);
}
if (idx !== array.length - 1) {
return (
<Fragment key={idx}>
{el}
<div className="mx-2">/</div>
</Fragment>
);
}
return <Fragment key={idx}>{el}</Fragment>;
}),
[pathElements],
);
return (
<>
<header className="dark:bg-dark-600 dark:border-dark-100 bg-light-600 border-light-800 fixed top-0 left-0 z-20 w-full border-b">
<div className="h-18 block px-6">
<div className="flex h-full flex-row place-content-between place-items-center">
<Button
aria-label="Menu"
className="focus:ring-width-2 focus:ring-blurple flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-0 focus:ring active:translate-y-px lg:hidden"
onClick={() => setOpened((open) => !open)}
>
<VscMenu size={24} />
</Button>
<div className="hidden md:flex md:flex-row">{breadcrumbs}</div>
<div className="flex flex-row place-items-center gap-4">
<Button
as="div"
className="dark:bg-dark-800 focus:ring-width-2 focus:ring-blurple rounded bg-white px-4 py-2.5 outline-0 focus:ring"
onClick={() => dialog?.toggle()}
>
<div className="flex flex-row place-items-center gap-4">
<VscSearch size={18} />
<span className="opacity-65">Search...</span>
<div className="opacity-65 flex flex-row place-items-center gap-2">
<FiCommand size={18} /> K
</div>
</div>
</Button>
<Button
aria-label="GitHub"
as="a"
className="focus:ring-width-2 focus:ring-blurple flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded rounded-full border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-0 focus:ring active:translate-y-px"
href="https://github.com/discordjs/discord.js"
rel="noopener noreferrer"
target="_blank"
>
<VscGithubInverted size={24} />
</Button>
<Button
aria-label="Toggle theme"
className="focus:ring-width-2 focus:ring-blurple flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded-full rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-0 focus:ring active:translate-y-px"
onClick={() => toggleTheme()}
>
<VscColorMode size={24} />
</Button>
</div>
</div>
</div>
</header>
<nav
className={`dark:bg-dark-600 dark:border-dark-100 border-light-800 fixed top-[73px] left-0 bottom-0 z-20 h-[calc(100vh_-_73px)] w-full border-r bg-white ${
opened ? 'block' : 'hidden'
} lg:w-76 lg:max-w-76 lg:block`}
>
<Scrollbars
autoHide
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="dark:bg-dark-100 bg-light-900 z-30 rounded" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute top-0.5 right-0.5 bottom-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<div className="flex flex-col gap-3 px-3 pt-3">
<MenuButton
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-0 focus:ring"
state={packageMenu}
>
<div className="flex flex-row place-content-between place-items-center">
<div className="flex flex-row place-items-center gap-3">
<VscPackage size={20} />
<span className="font-semibold">{packageName}</span>
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${
packageMenu.open ? 'rotate-180' : 'rotate-0'
}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="dark:bg-dark-600 border-light-800 dark:border-dark-100 focus:ring-width-2 focus:ring-blurple z-20 flex flex-col rounded border bg-white p-1 outline-0 focus:ring"
state={packageMenu}
>
{packageMenuItems}
</Menu>
<MenuButton
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-0 focus:ring"
state={versionMenu}
>
<div className="flex flex-row place-content-between place-items-center">
<div className="flex flex-row place-items-center gap-3">
<VscVersions size={20} />
<span className="font-semibold">{branchName}</span>
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${
versionMenu.open ? 'rotate-180' : 'rotate-0'
}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="dark:bg-dark-600 border-light-800 dark:border-dark-100 focus:ring-width-2 focus:ring-blurple z-20 flex flex-col rounded border bg-white p-1 outline-0 focus:ring"
state={versionMenu}
>
{versionMenuItems}
</Menu>
</div>
<SidebarItems members={data?.members ?? []} setOpened={setOpened} />
</Scrollbars>
</nav>
<main
className={`pt-18 lg:pl-76 ${
(data?.member?.kind === 'Class' || data?.member?.kind === 'Interface') &&
((data.member as ApiClassJSON | ApiInterfaceJSON).methods?.length ||
(data.member as ApiClassJSON | ApiInterfaceJSON).properties?.length)
? 'xl:pr-64'
: ''
}`}
>
<article className="dark:bg-dark-600 bg-light-600">
<div className="dark:bg-dark-800 relative z-10 min-h-[calc(100vh_-_70px)] bg-white p-6 pb-20 shadow">
{children}
</div>
<div className="h-76 md:h-52" />
<footer
className={`dark:bg-dark-600 h-76 lg:pl-84 bg-light-600 fixed bottom-0 left-0 right-0 md:h-52 md:pl-4 md:pr-16 ${
(data?.member?.kind === 'Class' || data?.member?.kind === 'Interface') &&
((data.member as ApiClassJSON | ApiInterfaceJSON).methods?.length ||
(data.member as ApiClassJSON | ApiInterfaceJSON).properties?.length)
? 'xl:pr-76'
: 'xl:pr-16'
}`}
>
<div className="mx-auto flex max-w-6xl flex-col place-items-center gap-12 pt-12 lg:place-content-center">
<div className="flex w-full flex-col place-content-between place-items-center gap-12 md:flex-row md:gap-0">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
rel="noopener noreferrer"
target="_blank"
title="Vercel"
>
<Image alt="Vercel" src={vercelLogo} />
</a>
<div className="flex flex-row gap-6 md:gap-12">
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold">Community</div>
<div className="flex flex-col gap-1">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discord.gg/djs"
rel="noopener noreferrer"
target="_blank"
>
Discord
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://github.com/discordjs/discord.js/discussions"
rel="noopener noreferrer"
target="_blank"
>
GitHub discussions
</a>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold">Project</div>
<div className="flex flex-col gap-1">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://github.com/discordjs/discord.js"
rel="noopener noreferrer"
target="_blank"
>
discord.js
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discordjs.guide"
rel="noopener noreferrer"
target="_blank"
>
discord.js guide
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discord-api-types.dev"
rel="noopener noreferrer"
target="_blank"
>
discord-api-types
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
</article>
</main>
<CmdKDialog currentPackageName={packageName} currentVersion={branchName} />
</>
);
}

View File

@@ -0,0 +1,31 @@
import { PrismAsyncLight } from 'react-syntax-highlighter';
import { vscDarkPlus, prism } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export function SyntaxHighlighter({ language = 'typescript', code }: { code: string; language?: string }) {
return (
<>
<div data-theme="dark">
<PrismAsyncLight
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
language={language}
style={vscDarkPlus}
wrapLines
wrapLongLines
>
{code}
</PrismAsyncLight>
</div>
<div data-theme="light">
<PrismAsyncLight
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
language={language}
style={prism}
wrapLines
wrapLongLines
>
{code}
</PrismAsyncLight>
</div>
</>
);
}

View File

@@ -0,0 +1,52 @@
import { useMemo, type ReactNode } from 'react';
export function Table({
rows,
columns,
columnStyles,
}: {
columnStyles?: Record<string, string>;
columns: string[];
rows: Record<string, ReactNode>[];
}) {
const cols = useMemo(
() =>
columns.map((column) => (
<th
className="border-light-900 dark:border-dark-100 break-normal border-b px-3 py-2 text-left text-sm"
key={column}
>
{column}
</th>
)),
[columns],
);
const data = useMemo(
() =>
rows.map((row, idx) => (
<tr className="[&>td]:last-of-type:border-0" key={idx}>
{Object.entries(row).map(([colName, val]) => (
<td
className={`border-light-900 dark:border-dark-100 border-b px-3 py-2 text-left text-sm ${
columnStyles?.[colName] ?? ''
}`}
key={colName}
>
{val}
</td>
))}
</tr>
)),
[columnStyles, rows],
);
return (
<table className="w-full border-collapse">
<thead>
<tr>{cols}</tr>
</thead>
<tbody>{data}</tbody>
</table>
);
}

View File

@@ -0,0 +1,87 @@
import type { ApiClassJSON, ApiInterfaceJSON } from '@discordjs/api-extractor-utils';
import { useMemo } from 'react';
import { VscListSelection, VscSymbolMethod, VscSymbolProperty } from 'react-icons/vsc';
export function TableOfContentItems({
methods,
properties,
}: {
methods: ApiClassJSON['methods'] | ApiInterfaceJSON['methods'];
properties: ApiClassJSON['properties'] | ApiInterfaceJSON['properties'];
}) {
const propertyItems = useMemo(
() =>
properties.map((prop) => (
<a
className="dark:border-dark-100 border-light-800 dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800 pl-6.5 focus:ring-width-2 focus:ring-blurple ml-[10px] border-l p-[5px] text-sm outline-0 focus:rounded focus:border-0 focus:ring"
href={`#${prop.name}`}
key={prop.name}
title={prop.name}
>
<span className="line-clamp-1">{prop.name}</span>
</a>
)),
[properties],
);
const methodItems = useMemo(
() =>
methods.map((member) => {
if (member.overloadIndex && member.overloadIndex > 1) {
return null;
}
const key = `${member.name}${
member.overloadIndex && member.overloadIndex > 1 ? `:${member.overloadIndex}` : ''
}`;
return (
<a
className="dark:border-dark-100 border-light-800 dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800 pl-6.5 focus:ring-width-2 focus:ring-blurple ml-[10px] flex flex-row place-items-center gap-2 border-l p-[5px] text-sm outline-0 focus:rounded focus:border-0 focus:ring"
href={`#${key}`}
key={key}
title={member.name}
>
<span className="line-clamp-1">{member.name}</span>
{member.overloadIndex && member.overloadIndex > 1 ? (
<span className="text-xs">{member.overloadIndex}</span>
) : null}
</a>
);
}),
[methods],
);
return (
<div className="flex flex-col break-all p-3 pb-8">
<div className="mt-4 ml-2 flex flex-row gap-2">
<VscListSelection size={25} />
<span className="font-semibold">Contents</span>
</div>
<div className="mt-5.5 ml-2 flex flex-col gap-2">
{propertyItems.length ? (
<div className="flex flex-col">
<div className="flex flex-row place-items-center gap-4">
<VscSymbolProperty size={20} />
<div className="p-3 pl-0">
<span className="font-semibold">Properties</span>
</div>
</div>
{propertyItems}
</div>
) : null}
{methodItems.length ? (
<div className="flex flex-col">
<div className="flex flex-row place-items-center gap-4">
<VscSymbolMethod size={20} />
<div className="p-3 pl-0">
<span className="font-semibold">Methods</span>
</div>
</div>
{methodItems}
</div>
) : null}
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import type { TypeParameterData } from '@discordjs/api-extractor-utils';
import { useMemo } from 'react';
import { HyperlinkedText } from './HyperlinkedText';
import { Table } from './Table';
import { TSDoc } from './tsdoc/TSDoc';
const rowElements = {
Name: 'font-mono whitespace-nowrap',
Constraints: 'font-mono whitespace-pre break-normal',
Default: 'font-mono whitespace-pre break-normal',
};
export function TypeParamTable({ data }: { data: TypeParameterData[] }) {
const rows = useMemo(
() =>
data.map((typeParam) => ({
Name: typeParam.name,
Constraints: <HyperlinkedText tokens={typeParam.constraintTokens} />,
Optional: typeParam.optional ? 'Yes' : 'No',
Default: <HyperlinkedText tokens={typeParam.defaultTokens} />,
Description: typeParam.commentBlock ? <TSDoc node={typeParam.commentBlock} /> : 'None',
})),
[data],
);
return (
<div className="overflow-x-auto">
<Table
columnStyles={rowElements}
columns={['Name', 'Constraints', 'Optional', 'Default', 'Description']}
rows={rows}
/>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import type { ApiClassJSON } from '@discordjs/api-extractor-utils';
import { DocContainer } from '../DocContainer';
import { ConstructorSection, MethodsSection, PropertiesSection } from '../Sections';
export function Class({ data }: { data: ApiClassJSON }) {
return (
<DocContainer
excerpt={data.excerpt}
extendsTokens={data.extendsTokens}
implementsTokens={data.implementsTokens}
kind={data.kind}
methods={data.methods}
name={data.name}
properties={data.properties}
summary={data.summary}
typeParams={data.typeParameters}
>
{data.constructor ? <ConstructorSection data={data.constructor} /> : null}
<PropertiesSection data={data.properties} />
<MethodsSection data={data.methods} />
</DocContainer>
);
}

View File

@@ -0,0 +1,28 @@
import type { ApiEnumJSON } from '@discordjs/api-extractor-utils';
import { Section } from '@discordjs/ui';
import { VscSymbolEnumMember } from 'react-icons/vsc';
import { useMedia } from 'react-use';
import { CodeListing, CodeListingSeparatorType } from '../CodeListing';
import { DocContainer } from '../DocContainer';
export function Enum({ data }: { data: ApiEnumJSON }) {
const matches = useMedia('(max-width: 768px)', true);
return (
<DocContainer excerpt={data.excerpt} kind={data.kind} name={data.name} summary={data.summary}>
<Section dense={matches} icon={<VscSymbolEnumMember size={20} />} padded title="Members">
<div className="flex flex-col gap-4">
{data.members.map((member) => (
<CodeListing
key={member.name}
name={member.name}
separator={CodeListingSeparatorType.Value}
summary={member.summary}
typeTokens={member.initializerTokens}
/>
))}
</div>
</Section>
</DocContainer>
);
}

View File

@@ -0,0 +1,62 @@
import type { ApiFunctionJSON } from '@discordjs/api-extractor-utils';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit';
import { useState } from 'react';
import { VscChevronDown, VscVersions } from 'react-icons/vsc';
import { DocContainer } from '../DocContainer';
import { ParametersSection } from '../Sections';
export function Function({ data }: { data: ApiFunctionJSON }) {
const [overloadIndex, setOverloadIndex] = useState(1);
const overloadedData = data.mergedSiblings[overloadIndex - 1]!;
const menu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
return (
<DocContainer
excerpt={overloadedData.excerpt}
kind={overloadedData.kind}
name={`${overloadedData.name}${
overloadedData.overloadIndex && overloadedData.overloadIndex > 1 ? ` (${overloadedData.overloadIndex})` : ''
}`}
subHeading={
data.mergedSiblings.length > 1 ? (
<div className="flex flex-row place-items-center gap-2">
<MenuButton
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-0 focus:ring"
state={menu}
>
<div className="flex flex-row place-content-between place-items-center gap-2">
<VscVersions size={20} />
<div>
<span className="font-semibold">{`Overload ${overloadIndex}`}</span>
{` of ${data.mergedSiblings.length}`}
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${menu.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="dark:bg-dark-600 border-light-800 dark:border-dark-100 focus:ring-width-2 focus:ring-blurple z-20 flex flex-col rounded border bg-white p-1 outline-0 focus:ring"
state={menu}
>
{data.mergedSiblings.map((_, idx) => (
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 cursor-pointer rounded bg-white p-3 text-sm outline-0 focus:ring"
key={idx}
onClick={() => setOverloadIndex(idx + 1)}
>
{`Overload ${idx + 1}`}
</MenuItem>
))}
</Menu>
</div>
) : null
}
summary={overloadedData.summary}
typeParams={overloadedData.typeParameters}
>
<ParametersSection data={overloadedData.parameters} />
</DocContainer>
);
}

View File

@@ -0,0 +1,20 @@
import type { ApiInterfaceJSON } from '@discordjs/api-extractor-utils';
import { DocContainer } from '../DocContainer';
import { MethodsSection, PropertiesSection } from '../Sections';
export function Interface({ data }: { data: ApiInterfaceJSON }) {
return (
<DocContainer
excerpt={data.excerpt}
kind={data.kind}
methods={data.methods}
name={data.name}
properties={data.properties}
summary={data.summary}
typeParams={data.typeParameters}
>
<PropertiesSection data={data.properties} />
<MethodsSection data={data.methods} />
</DocContainer>
);
}

View File

@@ -0,0 +1,14 @@
import type { ApiTypeAliasJSON } from '@discordjs/api-extractor-utils';
import { DocContainer } from '../DocContainer';
export function TypeAlias({ data }: { data: ApiTypeAliasJSON }) {
return (
<DocContainer
excerpt={data.excerpt}
kind={data.kind}
name={data.name}
summary={data.summary}
typeParams={data.typeParameters}
/>
);
}

View File

@@ -0,0 +1,6 @@
import type { ApiVariableJSON } from '@discordjs/api-extractor-utils';
import { DocContainer } from '../DocContainer';
export function Variable({ data }: { data: ApiVariableJSON }) {
return <DocContainer excerpt={data.excerpt} kind={data.kind} name={data.name} summary={data.summary} />;
}

View File

@@ -0,0 +1,56 @@
import { Alert } from '@discordjs/ui';
import { StandardTags } from '@microsoft/tsdoc';
import type { PropsWithChildren } from 'react';
export function Block({ children, title }: PropsWithChildren<{ title: string }>) {
return (
<div className="flex flex-col gap-2">
<h5 className="font-bold">{title}</h5>
{children}
</div>
);
}
export function ExampleBlock({
children,
exampleIndex,
}: PropsWithChildren<{ exampleIndex?: number | undefined }>): JSX.Element {
return <Block title={`Example ${exampleIndex ? exampleIndex : ''}`}>{children}</Block>;
}
export function DefaultValueBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Default value">{children}</Block>;
}
export function RemarksBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Remarks">{children}</Block>;
}
export function BlockComment({
children,
tagName,
index,
}: PropsWithChildren<{
index?: number | undefined;
tagName: string;
}>): JSX.Element {
switch (tagName.toUpperCase()) {
case StandardTags.example.tagNameWithUpperCase:
return <ExampleBlock exampleIndex={index}>{children}</ExampleBlock>;
case StandardTags.deprecated.tagNameWithUpperCase:
return (
<Alert title="Deprecated" type="danger">
{children}
</Alert>
);
case StandardTags.remarks.tagNameWithUpperCase:
return <RemarksBlock>{children}</RemarksBlock>;
case StandardTags.defaultValue.tagNameWithUpperCase:
return <DefaultValueBlock>{children}</DefaultValueBlock>;
case StandardTags.typeParam.tagNameWithUpperCase:
case StandardTags.param.tagNameWithUpperCase:
return <span>{children}</span>;
default: // TODO: Support more blocks in the future.
return <>{children}</>;
}
}

View File

@@ -0,0 +1,123 @@
import type {
AnyDocNodeJSON,
DocPlainTextJSON,
DocNodeContainerJSON,
DocLinkTagJSON,
DocFencedCodeJSON,
DocBlockJSON,
DocCommentJSON,
} from '@discordjs/api-extractor-utils';
import { DocNodeKind, StandardTags } from '@microsoft/tsdoc';
import Link from 'next/link';
import { Fragment, useCallback, type ReactNode } from 'react';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { BlockComment } from './BlockComment';
export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element {
const createNode = useCallback((node: AnyDocNodeJSON, idx?: number): ReactNode => {
let numberOfExamples = 0;
let exampleIndex = 0;
switch (node.kind) {
case DocNodeKind.PlainText:
return (
<span className="break-words" key={idx}>
{(node as DocPlainTextJSON).text}
</span>
);
case DocNodeKind.Paragraph:
return (
<span className="break-words leading-relaxed" key={idx}>
{(node as DocNodeContainerJSON).nodes.map((node, idx) => createNode(node, idx))}
</span>
);
case DocNodeKind.SoftBreak:
return <Fragment key={idx} />;
case DocNodeKind.LinkTag: {
const { codeDestination, urlDestination, text } = node as DocLinkTagJSON;
if (codeDestination) {
return (
<Link href={codeDestination.path} key={idx} prefetch={false}>
<a className="text-blurple focus:ring-width-2 focus:ring-blurple rounded font-mono outline-0 focus:ring">
{text ?? codeDestination.name}
</a>
</Link>
);
}
if (urlDestination) {
return (
<Link href={urlDestination} key={idx} prefetch={false}>
<a className="text-blurple focus:ring-width-2 focus:ring-blurple rounded font-mono outline-0 focus:ring">
{text ?? urlDestination}
</a>
</Link>
);
}
return null;
}
case DocNodeKind.CodeSpan: {
const { code } = node as DocFencedCodeJSON;
return (
<code className="font-mono text-sm" key={idx}>
{code}
</code>
);
}
case DocNodeKind.FencedCode: {
const { language, code } = node as DocFencedCodeJSON;
return <SyntaxHighlighter code={code} key={idx} language={language} />;
}
case DocNodeKind.ParamBlock:
case DocNodeKind.Block: {
const { tag } = node as DocBlockJSON;
if (tag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase) {
exampleIndex++;
}
const index = numberOfExamples > 1 ? exampleIndex : undefined;
return (
<BlockComment index={index} key={idx} tagName={tag.tagName}>
{(node as DocBlockJSON).content.map((node, idx) => createNode(node, idx))}
</BlockComment>
);
}
case DocNodeKind.Comment: {
const comment = node as DocCommentJSON;
if (!comment.customBlocks.length) {
return null;
}
// Cheat a bit by finding out how many comments we have beforehand...
numberOfExamples = comment.customBlocks.filter(
(block) => block.tag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase,
).length;
return <div key={idx}>{comment.customBlocks.map((node, idx) => createNode(node, idx))}</div>;
}
default:
// console.log(`Captured unknown node kind: ${node.kind}`);
return null;
}
}, []);
return (
<>
{node.kind === 'Paragraph' || node.kind === 'Section' ? (
<>{(node as DocNodeContainerJSON).nodes.map((node, idx) => createNode(node, idx))}</>
) : (
createNode(node)
)}
</>
);
}

View File

@@ -0,0 +1,15 @@
import { type DisclosureState, useDialogState } from 'ariakit';
import type { PropsWithChildren } from 'react';
import { createContext, useContext } from 'react';
export const CmdKContext = createContext<DisclosureState | null>(null);
export const CmdKProvider = ({ children }: PropsWithChildren) => {
const dialog = useDialogState();
return <CmdKContext.Provider value={dialog}>{children}</CmdKContext.Provider>;
};
export function useCmdK() {
return useContext(CmdKContext);
}

View File

@@ -0,0 +1,16 @@
import type { ApiItemJSON } from '@discordjs/api-extractor-utils';
import { createContext, useContext, type ReactNode } from 'react';
export const MemberContext = createContext<ApiItemJSON | undefined>(undefined);
export const MemberProvider = ({
member,
children,
}: {
children?: ReactNode | undefined;
member: ApiItemJSON | undefined;
}) => <MemberContext.Provider value={member}>{children}</MemberContext.Provider>;
export function useMember() {
return useContext(MemberContext);
}

View File

@@ -0,0 +1,25 @@
import { NextResponse, type NextRequest } from 'next/server';
import { PACKAGES } from './util/constants';
export default async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.includes('discord.js')) {
return NextResponse.redirect('https://discord.js.org/#/docs/discord.js');
}
if (PACKAGES.some((pkg) => request.nextUrl.pathname.includes(pkg))) {
const packageName = /\/docs\/packages\/([^/]+)\/.*/.exec(request.nextUrl.pathname)?.[1] ?? 'builders';
const res = await fetch(`https://docs.discordjs.dev/api/info?package=${packageName}`);
const data: string[] = await res.json();
const latestVersion = data.at(-2);
return NextResponse.redirect(
new URL(request.nextUrl.pathname.replace('stable', latestVersion ?? 'main'), request.url),
);
}
return NextResponse.redirect(new URL('/docs/packages', request.url));
}
export const config = {
matcher: ['/docs', '/docs/packages/discord.js(.*)?', '/docs/packages/:package/stable/:member*'],
};

View File

@@ -0,0 +1,22 @@
import Head from 'next/head';
import Link from 'next/link';
export default function FourOhFourPage() {
return (
<>
<Head>
<title key="title">discord.js | 404</title>
<meta content="discord.js | 404" key="og_title" property="og:title" />
</Head>
<div className="mx-auto flex h-full max-w-lg flex-col place-content-center place-items-center gap-8 py-16 px-8 lg:py-0 lg:px-6">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">404</h1>
<h2 className="text-[2rem] md:text-[3rem]">Not found.</h2>
<Link href="/docs/packages" prefetch={false}>
<a className="bg-blurple focus:ring-width-2 flex h-11 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded border-0 px-6 text-base font-semibold leading-none text-white no-underline outline-0 focus:ring focus:ring-white active:translate-y-px">
Take me back
</a>
</Link>
</div>
</>
);
}

View File

@@ -0,0 +1,32 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import NextProgress from 'next-progress';
import { ThemeProvider } from 'next-themes';
import '@unocss/reset/tailwind.css';
import '../styles/unocss.css';
import '../styles/cmdk.css';
import '../styles/main.css';
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title key="title">discord.js</title>
<meta content="minimum-scale=1, initial-scale=1, width=device-width" name="viewport" />
<meta content="#5865f2" name="theme-color" />
</Head>
<ThemeProvider
attribute="class"
defaultTheme="system"
disableTransitionOnChange
value={{
light: 'light',
dark: 'dark',
}}
>
<NextProgress color="#5865f2" options={{ showSpinner: false }} />
<Component {...pageProps} />
</ThemeProvider>
</>
);
}

View File

@@ -0,0 +1,32 @@
import { Html, Head, Main, NextScript } from 'next/document';
import { DESCRIPTION } from '~/util/constants';
export default function Document() {
return (
<Html lang="en">
<Head>
<link href="/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
<link href="/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png" />
<link href="/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png" />
<link href="/site.webmanifest" rel="manifest" />
<link color="#090a16" href="/safari-pinned-tab.svg" rel="mask-icon" />
<meta content="light dark" name="color-scheme" />
<meta content="discord.js" name="apple-mobile-web-app-title" />
<meta content="discord.js" name="application-name" />
<meta content="#090a16" name="msapplication-TileColor" />
<meta content={DESCRIPTION} key="description" name="description" />
<meta content="discord.js" property="og:site_name" />
<meta content="website" property="og:type" />
<meta content="discord.js" key="og_title" property="og:title" />
<meta content={DESCRIPTION} key="og_description" name="og:description" />
<meta content="https://discordjs.dev/open-graph.png" property="og:image" />
<meta content="summary_large_image" name="twitter:card" />
<meta content="@iCrawlToGo" name="twitter:creator" />
</Head>
<body className="dark:bg-dark-800 bg-white">
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -0,0 +1,284 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import process, { cwd } from 'node:process';
import {
findPackage,
getMembers,
type ApiItemJSON,
type ApiClassJSON,
type ApiFunctionJSON,
type ApiInterfaceJSON,
type ApiTypeAliasJSON,
type ApiVariableJSON,
type ApiEnumJSON,
} from '@discordjs/api-extractor-utils';
import { createApiModel } from '@discordjs/scripts';
import { ApiFunction, ApiItemKind, type ApiPackage } from '@microsoft/api-extractor-model';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { GetStaticPaths, GetStaticProps } from 'next/types';
import { MDXRemote } from 'next-mdx-remote';
import { serialize } from 'next-mdx-remote/serialize';
import { useMemo } from 'react';
import rehypeIgnore from 'rehype-ignore';
import rehypePrettyCode, { type Options } from 'rehype-pretty-code';
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import { getHighlighter } from 'shiki';
import shikiLangJavascript from 'shiki/languages/javascript.tmLanguage.json';
import shikiLangTypescript from 'shiki/languages/typescript.tmLanguage.json';
import shikiThemeDarkPlus from 'shiki/themes/dark-plus.json';
import shikiThemeLightPlus from 'shiki/themes/light-plus.json';
import { SidebarLayout, type SidebarLayoutProps } from '~/components/SidebarLayout';
import { Class } from '~/components/model/Class';
import { Enum } from '~/components/model/Enum';
import { Function } from '~/components/model/Function';
import { Interface } from '~/components/model/Interface';
import { TypeAlias } from '~/components/model/TypeAlias';
import { Variable } from '~/components/model/Variable';
import { CmdKProvider } from '~/contexts/cmdK';
import { MemberProvider } from '~/contexts/member';
import { PACKAGES } from '~/util/constants';
import { findMember, findMemberByKey } from '~/util/model.server';
// import { miniSearch } from '~/util/search';
export const getStaticPaths: GetStaticPaths = async () => {
const pkgs = (
await Promise.all(
PACKAGES.map(async (packageName) => {
try {
let data: any[] = [];
let versions: string[] = [];
if (process.env.NEXT_PUBLIC_LOCAL_DEV) {
const res = await readFile(join(cwd(), '..', packageName, 'docs', 'docs.api.json'), 'utf8');
data = JSON.parse(res);
} else {
const response = await fetch(`https://docs.discordjs.dev/api/info?package=${packageName}`);
versions = await response.json();
versions = versions.slice(-2);
for (const version of versions) {
const res = await fetch(`https://docs.discordjs.dev/docs/${packageName}/${version}.api.json`);
data = [...data, await res.json()];
}
}
if (Array.isArray(data)) {
const models = data.map((innerData) => createApiModel(innerData));
const pkgs = models.map((model) => findPackage(model, packageName)) as ApiPackage[];
return [
...versions.map((version) => ({ params: { slug: ['packages', packageName, version] } })),
...pkgs.flatMap((pkg, idx) =>
getMembers(pkg, versions[idx]!).map((member) => {
if (member.kind === ApiItemKind.Function && member.overloadIndex && member.overloadIndex > 1) {
return {
params: {
slug: [
'packages',
packageName,
versions[idx]!,
`${member.name}:${member.overloadIndex}:${member.kind}`,
],
},
};
}
return {
params: {
slug: ['packages', packageName, versions[idx]!, `${member.name}:${member.kind}`],
},
};
}),
),
];
}
const model = createApiModel(data);
const pkg = findPackage(model, packageName)!;
return [
{ params: { slug: ['packages', packageName, 'main'] } },
...getMembers(pkg, 'main').map((member) => {
if (member.kind === ApiItemKind.Function && member.overloadIndex && member.overloadIndex > 1) {
return {
params: {
slug: ['packages', packageName, 'main', `${member.name}:${member.overloadIndex}:${member.kind}`],
},
};
}
return { params: { slug: ['packages', packageName, 'main', `${member.name}:${member.kind}`] } };
}),
];
} catch {
return { params: { slug: [] } };
}
}),
)
).flat();
return {
paths: pkgs,
fallback: true,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const [path, packageName = 'builders', branchName = 'main', member] = params!.slug as string[];
if (path !== 'packages' || !PACKAGES.includes(packageName)) {
return {
notFound: true,
};
}
const [memberName, overloadIndex] = member?.split(':') ?? [];
try {
const readme = await readFile(join(cwd(), '..', packageName, 'README.md'), 'utf8');
const mdxSource = await serialize(readme, {
mdxOptions: {
remarkPlugins: [remarkGfm],
remarkRehypeOptions: { allowDangerousHtml: true },
rehypePlugins: [
rehypeRaw,
rehypeIgnore,
rehypeSlug,
[
rehypePrettyCode,
{
theme: {
dark: shikiThemeDarkPlus,
light: shikiThemeLightPlus,
},
getHighlighter: async (options?: Partial<Options>) =>
getHighlighter({
...options,
langs: [
// @ts-expect-error: Working as intended
{ id: 'javascript', aliases: ['js'], scopeName: 'source.js', grammar: shikiLangJavascript },
// @ts-expect-error: Working as intended
{ id: 'typescript', aliases: ['ts'], scopeName: 'source.ts', grammar: shikiLangTypescript },
],
}),
},
],
],
format: 'md',
},
});
let data;
if (process.env.NEXT_PUBLIC_LOCAL_DEV) {
const res = await readFile(join(cwd(), '..', packageName, 'docs', 'docs.api.json'), 'utf8');
data = JSON.parse(res);
} else {
const res = await fetch(`https://docs.discordjs.dev/docs/${packageName}/${branchName}.api.json`);
data = await res.json();
}
const model = createApiModel(data);
const pkg = findPackage(model, packageName);
// eslint-disable-next-line prefer-const
let { containerKey, name } = findMember(model, packageName, memberName, branchName) ?? {};
if (name && overloadIndex && !Number.isNaN(Number.parseInt(overloadIndex, 10))) {
containerKey = ApiFunction.getContainerKey(name, Number.parseInt(overloadIndex, 10));
}
return {
props: {
packageName,
branchName,
data: {
members: pkg
? getMembers(pkg, branchName).filter((item) => item.overloadIndex === null || item.overloadIndex <= 1)
: [],
member:
memberName && containerKey ? findMemberByKey(model, packageName, containerKey, branchName) ?? null : null,
source: mdxSource,
},
},
revalidate: 3_600,
};
} catch (error_) {
const error = error_ as Error;
console.error(error);
return {
props: {
error: error.message,
},
revalidate: 1,
};
}
};
const member = (props?: ApiItemJSON | undefined) => {
switch (props?.kind) {
case 'Class':
return <Class data={props as ApiClassJSON} />;
case 'Function':
return <Function data={props as ApiFunctionJSON} key={props.containerKey} />;
case 'Interface':
return <Interface data={props as ApiInterfaceJSON} />;
case 'TypeAlias':
return <TypeAlias data={props as ApiTypeAliasJSON} />;
case 'Variable':
return <Variable data={props as ApiVariableJSON} />;
case 'Enum':
return <Enum data={props as ApiEnumJSON} />;
default:
return <div>Cannot render that item type</div>;
}
};
export default function SlugPage(props: Partial<SidebarLayoutProps & { error?: string }>) {
const router = useRouter();
const name = useMemo(
() => `discord.js${props.data?.member?.name ? ` | ${props.data.member.name}` : ''}`,
[props.data?.member?.name],
);
const ogTitle = useMemo(
() => `${props.packageName ?? 'discord.js'}${props.data?.member?.name ? ` | ${props.data.member.name}` : ''}`,
[props.packageName, props.data?.member?.name],
);
if (router.isFallback) {
return null;
}
// Just in case
// return <iframe src="https://discord.js.org" style={{ border: 0, height: '100%', width: '100%' }}></iframe>;
return props.error ? (
<div className="flex h-full max-h-full w-full max-w-full flex-row">{props.error}</div>
) : (
<CmdKProvider>
<MemberProvider member={props.data?.member}>
<SidebarLayout {...props}>
{props.data?.member ? (
<>
<Head>
<title key="title">{name}</title>
<meta content={ogTitle} key="og_title" property="og:title" />
</Head>
{member(props.data.member)}
</>
) : props.data?.source ? (
<div className="prose max-w-none">
<MDXRemote {...props.data.source} />
</div>
) : null}
</SidebarLayout>
</MemberProvider>
</CmdKProvider>
);
}
export const config = {
unstable_includeFiles: ['../{builders,collection,proxy,rest,util,voice,ws}/README.md'],
};

View File

@@ -0,0 +1,93 @@
import Link from 'next/link';
import type { GetStaticPaths, GetStaticProps } from 'next/types';
import { VscArrowLeft, VscArrowRight, VscVersions } from 'react-icons/vsc';
import { PACKAGES } from '~/util/constants';
interface VersionProps {
data: {
versions: string[];
};
packageName: string;
}
export const getStaticPaths: GetStaticPaths = () => {
const versions = PACKAGES.map((packageName) => ({ params: { package: packageName } }));
return {
paths: versions,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const packageName = params!.package as string;
if (!PACKAGES.includes(packageName)) {
return {
notFound: true,
};
}
try {
const res = await fetch(`https://docs.discordjs.dev/api/info?package=${packageName}`);
const data: string[] = await res.json();
if (!data.length) {
return {
notFound: true,
};
}
return {
props: {
packageName,
data: {
versions: data.reverse(),
},
},
revalidate: 3_600,
};
} catch (error_) {
const error = error_ as Error;
console.error(error);
return {
props: {
error: error.message,
},
revalidate: 1,
};
}
};
export default function VersionsRoute(props: Partial<VersionProps> & { error?: string }) {
return props.error ? (
<div className="min-w-xs sm:w-md mx-auto flex h-full flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
{props.error}
</div>
) : (
<div className="min-w-xs sm:w-md mx-auto flex h-full flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
<div className="flex grow flex-col place-content-center gap-4">
<h1 className="text-2xl font-semibold">Select a version:</h1>
{props.data?.versions.map((version) => (
<Link href={`/docs/packages/${props.packageName}/${version}`} key={version} prefetch={false}>
<a className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 focus:ring-width-2 focus:ring-blurple flex flex h-11 transform-gpu cursor-pointer select-none appearance-none flex-col place-content-center rounded border border-neutral-300 bg-transparent p-4 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 focus:ring active:translate-y-px active:bg-neutral-200 dark:text-white">
<div className="flex flex-row place-content-between place-items-center gap-4">
<div className="flex flex-row place-content-between place-items-center gap-4">
<VscVersions size={25} />
<h2 className="font-semibold">{version}</h2>
</div>
<VscArrowRight size={20} />
</div>
</a>
</Link>
)) ?? null}
<Link href="/docs/packages" prefetch={false}>
<a className="bg-blurple focus:ring-width-2 flex h-11 transform-gpu cursor-pointer select-none appearance-none place-items-center gap-2 place-self-center rounded border-0 px-4 text-base font-semibold leading-none text-white no-underline outline-0 focus:ring focus:ring-white active:translate-y-px">
<VscArrowLeft size={20} /> Go back
</a>
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,113 @@
import { Button } from 'ariakit/button';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { GetStaticProps } from 'next/types';
import { useCallback, type MouseEvent } from 'react';
import { VscArrowLeft, VscArrowRight, VscPackage } from 'react-icons/vsc';
import { PACKAGES } from '~/util/constants';
interface PackageProps {
data: {
versions: { packageName: string; version: string }[];
};
}
export const getStaticProps: GetStaticProps = async () => {
try {
const versions = await Promise.all(
PACKAGES.map(async (pkg) => {
const response = await fetch(`https://docs.discordjs.dev/api/info?package=${pkg}`);
const versions = await response.json();
const latestVersion = versions.at(-2);
return { packageName: pkg, version: latestVersion };
}),
);
return {
props: {
versions,
},
revalidate: 3_600,
};
} catch (error_) {
const error = error_ as Error;
console.error(error);
return {
props: {
error: error.message,
},
revalidate: 1,
};
}
};
export default function PackagesRoute(props: Partial<PackageProps> & { error?: string }) {
const router = useRouter();
const findLatestVersion = useCallback(
(pkg: string) => props.data?.versions.find((version) => version.packageName === pkg),
[props.data?.versions],
);
const handleClick = async (ev: MouseEvent<HTMLDivElement>, packageName: string) => {
ev.stopPropagation();
void router.push(`/docs/packages/${packageName}`);
};
return props.error ? (
<div className="min-w-xs sm:w-md mx-auto flex h-full flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
{props.error}
</div>
) : (
<div className="min-w-xs sm:w-md mx-auto flex h-full flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
<div className="flex grow flex-col place-content-center gap-4">
<h1 className="text-2xl font-semibold">Select a package:</h1>
<a
className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 focus:ring-width-2 focus:ring-blurple flex h-11 transform-gpu cursor-pointer select-none appearance-none place-content-between rounded border border-neutral-300 bg-transparent p-4 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 focus:ring active:translate-y-px active:bg-neutral-200 dark:text-white"
href="https://discord.js.org/#/docs/discord.js"
>
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex flex-row place-content-between place-items-center gap-4">
<VscPackage size={25} />
<h2 className="font-semibold">discord.js</h2>
</div>
</div>
<VscArrowRight size={20} />
</div>
</a>
{PACKAGES.map((pkg) => (
<Link href={`/docs/packages/${pkg}/${findLatestVersion(pkg)?.version ?? 'main'}`} key={pkg} prefetch={false}>
<a className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 focus:ring-width-2 focus:ring-blurple flex h-11 transform-gpu cursor-pointer select-none appearance-none place-content-between rounded border border-neutral-300 bg-transparent p-4 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 focus:ring active:translate-y-px active:bg-neutral-200 dark:text-white">
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex flex-row place-content-between place-items-center gap-4">
<VscPackage size={25} />
<h2 className="font-semibold">{pkg}</h2>
</div>
<Link href={`/docs/packages/${pkg}`} prefetch={false}>
<Button
as="div"
className="bg-blurple focus:ring-width-2 flex h-6 transform-gpu cursor-pointer select-none appearance-none place-content-center place-items-center rounded border-0 px-2 text-xs font-semibold leading-none text-white outline-0 focus:ring focus:ring-white active:translate-y-px"
onClick={async (ev: MouseEvent<HTMLDivElement>) => handleClick(ev, pkg)}
role="link"
>
Select version
</Button>
</Link>
</div>
<VscArrowRight size={20} />
</div>
</a>
</Link>
))}
<Link href="/" prefetch={false}>
<a className="bg-blurple focus:ring-width-2 flex h-11 transform-gpu cursor-pointer select-none appearance-none place-items-center gap-2 place-self-center rounded border-0 px-4 text-base font-semibold leading-none text-white no-underline outline-0 focus:ring focus:ring-white active:translate-y-px">
<VscArrowLeft size={20} /> Go back
</a>
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,61 @@
import Image from 'next/future/image';
import Link from 'next/link';
import { FiExternalLink } from 'react-icons/fi';
import vercelLogo from '../assets/powered-by-vercel.svg';
import { SyntaxHighlighter } from '~/components/SyntaxHighlighter';
import { CODE_EXAMPLE } from '~/util/constants';
export default function IndexRoute() {
return (
<div className="mx-auto flex max-w-6xl flex-col place-items-center gap-12 py-16 px-8 lg:h-full lg:place-content-center lg:py-0 lg:px-6">
<div className="flex flex-col place-items-center gap-10 lg:flex-row lg:gap-6">
<div className="flex max-w-lg flex-col gap-3 lg:mr-8">
<h1 className="text-3xl font-black leading-tight sm:text-5xl sm:leading-tight">
The <span className="bg-blurple relative rounded py-1 px-3 text-white">most popular</span> way to build
Discord <br /> bots.
</h1>
<p className="my-6 leading-normal text-neutral-700 dark:text-neutral-300">
discord.js is a powerful node.js module that allows you to interact with the Discord API very easily. It
takes a much more object-oriented approach than most other JS Discord libraries, making your bot&apos;s code
significantly tidier and easier to comprehend.
</p>
<div className="flex flex-row gap-4">
<Link href="/docs" prefetch={false}>
<a className="bg-blurple focus:ring-width-2 flex h-11 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded border-0 px-6 text-base font-semibold leading-none text-white no-underline outline-0 focus:ring focus:ring-white active:translate-y-px">
Docs
</a>
</Link>
<a
className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 border-light-900 hover:bg-light-200 active:bg-light-300 focus:ring-blurple focus:ring-width-2 flex h-11 transform-gpu cursor-pointer select-none appearance-none place-items-center gap-2 rounded border bg-transparent px-4 text-base font-semibold leading-none text-black no-underline outline-0 focus:ring active:translate-y-px dark:text-white"
href="https://discordjs.guide"
rel="noopener noreferrer"
target="_blank"
>
Guide <FiExternalLink />
</a>
<a
className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 border-light-900 hover:bg-light-200 active:bg-light-300 focus:ring-blurple focus:ring-width-2 flex h-11 transform-gpu cursor-pointer select-none appearance-none appearance-none place-items-center gap-2 rounded border bg-transparent px-4 text-base font-semibold leading-none text-black no-underline outline-0 focus:ring active:translate-y-px dark:text-white"
href="https://github.com/discordjs/discord.js"
rel="noopener noreferrer"
target="_blank"
>
GitHub <FiExternalLink />
</a>
</div>
</div>
<SyntaxHighlighter code={CODE_EXAMPLE} />
</div>
<div className="flex place-content-center">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
rel="noopener noreferrer"
target="_blank"
title="Vercel"
>
<Image alt="Vercel" src={vercelLogo} />
</a>
</div>
</div>
);
}

View File

@@ -0,0 +1,3 @@
[data-backdrop] {
background-color: rgb(0 0 0 / 35%);
}

View File

@@ -0,0 +1,65 @@
@import url('https://rsms.me/inter/inter.css');
:root {
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
@supports (font-variation-settings: normal) {
:root {
font-family: 'Inter var', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
}
}
html,
body {
height: 100%;
color-scheme: light;
}
#__next {
height: 100%;
}
[data-theme='dark'] {
display: none;
}
.dark [data-theme='dark'] {
display: block;
}
.dark [data-theme='light'] {
display: none;
}
pre[data-theme='light'] {
background: #ffffff;
border: 1px solid #dddddd;
}
pre[data-theme='dark'] {
background: #1e1e1e;
}
pre {
font-size: 13px !important;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
tab-size: 4;
hyphens: none;
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 4px;
}
code {
font-family: 'JetBrains Mono', monospace !important;
}

View File

@@ -0,0 +1,22 @@
export const PACKAGES = ['builders', 'collection', 'proxy', 'rest', 'util', 'voice', 'ws'];
export const DESCRIPTION =
"discord.js is a powerful node.js module that allows you to interact with the Discord API very easily. It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.";
export const CODE_EXAMPLE = `import { Client, GatewayIntentBits } from 'discord.js';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on('ready', () => {
console.log(\`Logged in as \${client.user.tag}!\`);
});
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
await interaction.reply('Pong!');
}
});
await client.login(TOKEN);`;

View File

@@ -0,0 +1,4 @@
export const fetcher = async (url: string) => {
const res = await fetch(url);
return res.json();
};

View File

@@ -0,0 +1,33 @@
import { findPackage, ApiNodeJSONEncoder } from '@discordjs/api-extractor-utils';
import type { ApiEntryPoint, ApiModel } from '@microsoft/api-extractor-model';
export function findMemberByKey(model: ApiModel, packageName: string, containerKey: string, version: string) {
const pkg = findPackage(model, packageName)!;
const member = (pkg.members[0] as ApiEntryPoint).tryGetMemberByKey(containerKey);
if (!member) {
return undefined;
}
return ApiNodeJSONEncoder.encode(model, member, version);
}
export function findMember(
model: ApiModel,
packageName: string,
memberName: string | undefined,
version: string,
): ReturnType<typeof ApiNodeJSONEncoder['encode']> | undefined {
if (!memberName) {
return undefined;
}
const pkg = findPackage(model, packageName)!;
const member = (pkg.members[0] as ApiEntryPoint).findMembersByName(memberName)[0];
if (!member) {
return undefined;
}
return ApiNodeJSONEncoder.encode(model, member, version);
}

View File

@@ -0,0 +1,6 @@
import MeiliSearch from 'meilisearch';
export const client = new MeiliSearch({
host: 'https://search.discordjs.dev',
apiKey: 'b51923c6abb574b1e97be9a03dc6414b6c69fb0c5696d0ef01a82b0f77d223db',
});

View File

@@ -0,0 +1,21 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": true
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.test.ts",
"**/*.test.js",
"**/*.test.mjs",
"**/*.spec.ts",
"**/*.spec.js",
"**/*.spec.mjs"
],
"exclude": []
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"noEmit": true,
"allowJs": false,
"incremental": true,
"skipLibCheck": true,
"paths": {
"~/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts", "types.d.ts"],
"exclude": ["node_modules"]
}

1
apps/website/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '*.css';