refactor: docs (#10126)

This commit is contained in:
Noel
2024-02-29 04:37:52 +01:00
committed by GitHub
parent 0f9017ef95
commit 18cce83d80
192 changed files with 8116 additions and 6321 deletions

View File

@@ -113,6 +113,24 @@ jobs:
package: ${{ steps.extract-tag.outputs.package }}
version: ${{ steps.extract-tag.outputs.semver }}
- name: Upload split documentation to blob storage
if: ${{ env.REF_TYPE == 'tag' && (!inputs.ref || inputs.ref == 'main') }}
env:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./packages/actions/src/uploadSplitDocumentation
with:
package: ${{ steps.extract-tag.outputs.package }}
version: ${{ steps.extract-tag.outputs.semver }}
- name: Upload split documentation to blob storage
if: ${{ env.REF_TYPE == 'tag' && inputs.ref && inputs.ref != 'main' }}
env:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./main/packages/actions/src/uploadSplitDocumentation
with:
package: ${{ steps.extract-tag.outputs.package }}
version: ${{ steps.extract-tag.outputs.semver }}
- name: Move docs to correct directory
if: ${{ env.REF_TYPE == 'tag' }}
env:
@@ -141,6 +159,18 @@ jobs:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./main/packages/actions/src/uploadDocumentation
- name: Upload split documentation to blob storage
if: ${{ env.REF_TYPE == 'branch' && (!inputs.ref || inputs.ref == 'main') }}
env:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./packages/actions/src/uploadSplitDocumentation
- name: Upload split documentation to blob storage
if: ${{ env.REF_TYPE == 'branch' && inputs.ref && inputs.ref != 'main' }}
env:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./main/packages/actions/src/uploadSplitDocumentation
- name: Move docs to correct directory
if: ${{ env.REF_TYPE == 'branch' }}
run: |

View File

@@ -8,7 +8,6 @@
"eamodio.gitlens",
"christian-kohler.npm-intellisense",
"christian-kohler.path-intellisense",
"antfu.unocss",
"unifiedjs.vscode-mdx"
]
}

View File

@@ -26,6 +26,7 @@
"npm.packageManager": "pnpm",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"unocss.disable": true,
"deno.enable": false,
"deno.enablePaths": ["./packages/create-discord-bot/template/Deno"],
"deno.lint": false,

View File

@@ -47,8 +47,8 @@
"@code-hike/mdx": "^0.9.0",
"@discordjs/ui": "workspace:^",
"@react-icons/all-files": "^4.1.0",
"@vercel/analytics": "^1.1.3",
"@vercel/edge-config": "^0.4.1",
"@vercel/analytics": "^1.2.2",
"@vercel/edge-config": "^1.1.0",
"@vercel/og": "^0.6.2",
"ariakit": "2.0.0-next.44",
"cmdk": "^0.2.1",
@@ -70,28 +70,28 @@
"@testing-library/user-event": "^14.5.2",
"@types/html-escaper": "^3.0.2",
"@types/node": "18.18.8",
"@types/react": "^18.2.54",
"@types/react-dom": "^18.2.18",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"@unocss/eslint-plugin": "^0.58.5",
"@unocss/postcss": "^0.58.5",
"@unocss/reset": "^0.58.5",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.2.2",
"@vitest/coverage-v8": "^1.3.1",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint-config-neon": "^0.1.58",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.59",
"eslint-formatter-pretty": "^6.0.1",
"happy-dom": "^13.3.8",
"happy-dom": "^13.6.2",
"hast-util-to-string": "^2.0.0",
"hastscript": "^8.0.0",
"html-escaper": "^3.0.3",
"postcss": "^8.4.34",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"turbo": "^1.12.2",
"turbo": "^1.12.4",
"typescript": "^5.3.3",
"unocss": "^0.58.5",
"vercel": "^33.4.1",
"vitest": "^1.2.2"
"vercel": "^33.5.3",
"vitest": "^1.3.1"
},
"engines": {
"node": ">=18"

View File

@@ -1,2 +1 @@
NEXT_PUBLIC_LOCAL_DEV=true
METADATA_BASE_URL=http://localhost:3000

View File

@@ -28,3 +28,5 @@ src/styles/unocss.css
lighthouse-results
.vercel
old_src

View File

@@ -1,2 +1,5 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');
module.exports = {
...require('../../.prettierrc.json'),
plugins: ['prettier-plugin-tailwindcss'],
};

View File

@@ -1,4 +1,5 @@
import bundleAnalyzer from '@next/bundle-analyzer';
import localesPlugin from '@react-aria/optimize-locales-plugin';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
@@ -6,18 +7,26 @@ const withBundleAnalyzer = bundleAnalyzer({
export default withBundleAnalyzer({
reactStrictMode: true,
experimental: {
typedRoutes: true,
serverComponentsExternalPackages: ['@rushstack/node-core-library', '@discordjs/api-extractor-model', 'jju'],
},
images: {
dangerouslyAllowSVG: true,
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; frame-src 'none'; sandbox;",
},
poweredByHeader: false,
env: {
MAX_FETCH_SIZE: '5',
logging: {
fetches: {
fullUrl: true,
},
},
experimental: {
ppr: false,
},
webpack(config, { isServer }) {
if (!isServer) {
// Don't include any locale strings in the client JS bundle.
config.plugins.push(localesPlugin.webpack({ locales: [] }));
}
return config;
},
async redirects() {
return [

View File

@@ -46,58 +46,60 @@
},
"homepage": "https://discord.js.org",
"dependencies": {
"@discordjs/api-extractor-model": "workspace:^",
"@discordjs/api-extractor-utils": "workspace:^",
"@discordjs/scripts": "workspace:^",
"@discordjs/ui": "workspace:^",
"@microsoft/tsdoc": "^0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@react-icons/all-files": "^4.1.0",
"@vercel/analytics": "^1.1.3",
"@vercel/edge-config": "^0.4.1",
"@vercel/analytics": "^1.2.2",
"@vercel/blob": "^0.22.1",
"@vercel/edge-config": "^1.1.0",
"@vercel/og": "^0.6.2",
"@vercel/postgres": "^0.7.2",
"ariakit": "2.0.0-next.44",
"bright": "^0.8.4",
"class-variance-authority": "^0.7.0",
"cmdk": "^0.2.1",
"geist": "^1.2.2",
"jotai": "^2.7.0",
"lucide-react": "^0.343.0",
"meilisearch": "^0.37.0",
"next": "14.1.0",
"next": "14.1.1-canary.80",
"next-mdx-remote": "^4.4.1",
"next-themes": "^0.2.1",
"overlayscrollbars": "^2.5.0",
"overlayscrollbars-react": "^0.5.4",
"react": "^18.2.0",
"react-custom-scrollbars-2": "^4.5.0",
"react-aria-components": "^1.1.1",
"react-dom": "^18.2.0",
"react-use": "^17.5.0",
"rehype-slug": "^5.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.33.2",
"swr": "^2.2.4"
"usehooks-ts": "^2.15.1",
"vaul": "^0.9.0"
},
"devDependencies": {
"@next/bundle-analyzer": "14.1.0",
"@next/bundle-analyzer": "14.1.1-canary.80",
"@react-aria/optimize-locales-plugin": "^1.0.2",
"@shikijs/rehype": "1.1.7",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "18.18.8",
"@types/react": "^18.2.54",
"@types/react-dom": "^18.2.18",
"@unocss/eslint-plugin": "^0.58.5",
"@unocss/postcss": "^0.58.5",
"@unocss/reset": "^0.58.5",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.2.2",
"@vitest/coverage-v8": "^1.3.1",
"autoprefixer": "^10.4.17",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint-config-neon": "^0.1.58",
"eslint": "^8.57.0",
"eslint-config-neon": "^0.1.59",
"eslint-formatter-pretty": "^6.0.1",
"happy-dom": "^13.3.8",
"postcss": "^8.4.34",
"happy-dom": "^13.6.2",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"turbo": "^1.12.2",
"prettier-plugin-tailwindcss": "^0.5.11",
"remark-gfm": "^3.0.1",
"remark-rehype": "^11.1.0",
"shiki": "1.1.7",
"tailwindcss": "^3.4.1",
"turbo": "^1.12.4",
"typescript": "^5.3.3",
"vercel": "^33.4.1",
"vitest": "^1.2.2"
"vercel": "^33.5.3",
"vitest": "^1.3.1"
},
"engines": {
"node": ">=18"

View File

@@ -1,5 +1,6 @@
module.exports = {
plugins: {
'@unocss/postcss': {},
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,28 +0,0 @@
'use client';
import { Analytics } from '@vercel/analytics/react';
import { inter } from '~/util/fonts';
import { Providers } from './providers';
import '~/styles/cmdk.css';
import '~/styles/main.css';
export default function GlobalError({ error }: { readonly error: Error }) {
console.error(error);
return (
<html className={inter.variable} lang="en" suppressHydrationWarning>
<body className="bg-light-600 dark:bg-dark-600 dark:text-light-900">
<Providers>
<main className="mx-auto max-w-2xl min-h-screen">
<div className="mx-auto max-w-lg min-h-screen flex flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">500</h1>
<h2 className="text-[2rem] md:text-[3rem]">Error.</h2>
</div>
</main>
</Providers>
<Analytics />
</body>
</html>
);
}

View File

@@ -1,8 +0,0 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { fetchVersions } from '~/app/docAPI';
export async function GET(req: NextRequest) {
const packageName = req.nextUrl.pathname.split('/').slice(2, 3)[0] ?? 'discord.js';
return NextResponse.json(await fetchVersions(packageName));
}

View File

@@ -1,170 +0,0 @@
/* eslint-disable react/no-unknown-property */
import type { ApiItemKind } from '@discordjs/api-extractor-model';
import { ImageResponse } from '@vercel/og';
import type { NextRequest } from 'next/server';
import { resolvePackageName } from '~/util/resolvePackageName';
export const runtime = 'edge';
const fonts = Promise.all([
fetch(new URL('../../../assets/fonts/Inter-Regular.ttf', import.meta.url)).then(async (res) => res.arrayBuffer()),
fetch(new URL('../../../assets/fonts/Inter-Bold.ttf', import.meta.url)).then(async (res) => res.arrayBuffer()),
]);
function resolveIcon(icon: keyof typeof ApiItemKind, size = 88) {
switch (icon) {
case 'Class':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path d="M11.34 9.71h.71l2.67-2.67v-.71L13.38 5h-.7l-1.82 1.81h-5V5.56l1.86-1.85V3l-2-2H5L1 5v.71l2 2h.71l1.14-1.15v5.79l.5.5H10v.52l1.33 1.34h.71l2.67-2.67v-.71L13.37 10h-.7l-1.86 1.85h-5v-4H10v.48l1.34 1.38zm1.69-3.65l.63.63-2 2-.63-.63 2-2zm0 5l.63.63-2 2-.63-.63 2-2zM3.35 6.65l-1.29-1.3 3.29-3.29 1.3 1.29-3.3 3.3z" />
</svg>
);
case 'Enum':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path
clipRule="evenodd"
d="M14 2H8L7 3v3h1V3h6v5h-4v1h4l1-1V3l-1-1zM9 6h4v1H9.41L9 6.59V6zM7 7H2L1 8v5l1 1h6l1-1V8L8 7H7zm1 6H2V8h6v5zM3 9h4v1H3V9zm0 2h4v1H3v-1zm6-7h4v1H9V4z"
fillRule="evenodd"
/>
</svg>
);
case 'EnumMember':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path
clipRule="evenodd"
d="M7 3l1-1h6l1 1v5l-1 1h-4V8h4V3H8v3H7V3zm2 6V8L8 7H2L1 8v5l1 1h6l1-1V9zM8 8v5H2V8h6zm1.414-1L9 6.586V6h4v1H9.414zM9 4h4v1H9V4zm-2 6H3v1h4v-1z"
fillRule="evenodd"
/>
</svg>
);
case 'Interface':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path d="M11.496 4a3.49 3.49 0 0 0-3.46 3h-3.1a2 2 0 1 0 0 1h3.1a3.5 3.5 0 1 0 3.46-4zm0 6a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z" />
</svg>
);
case 'TypeAlias':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path d="M14.45 4.5l-5-2.5h-.9l-7 3.5-.55.89v4.5l.55.9 5 2.5h.9l7-3.5.55-.9v-4.5l-.55-.89zm-8 8.64l-4.5-2.25V7.17l4.5 2v3.97zm.5-4.8L2.29 6.23l6.66-3.34 4.67 2.34-6.67 3.11zm7 1.55l-6.5 3.25V9.21l6.5-3v3.68z" />
</svg>
);
case 'Variable':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path
clipRule="evenodd"
d="M2 5h2V4H1.5l-.5.5v8l.5.5H4v-1H2V5zm12.5-1H12v1h2v7h-2v1h2.5l.5-.5v-8l-.5-.5zm-2.74 2.57L12 7v2.51l-.3.45-4.5 2h-.46l-2.5-1.5-.24-.43v-2.5l.3-.46 4.5-2h.46l2.5 1.5zM5 9.71l1.5.9V9.28L5 8.38v1.33zm.58-2.15l1.45.87 3.39-1.5-1.45-.87-3.39 1.5zm1.95 3.17l3.5-1.56v-1.4l-3.5 1.55v1.41z"
fillRule="evenodd"
/>
</svg>
);
case 'Property':
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path d="M2.807 14.975a1.75 1.75 0 0 1-1.255-.556 1.684 1.684 0 0 1-.544-1.1A1.72 1.72 0 0 1 1.36 12.1c1.208-1.27 3.587-3.65 5.318-5.345a4.257 4.257 0 0 1 .048-3.078 4.095 4.095 0 0 1 1.665-1.969 4.259 4.259 0 0 1 4.04-.36l.617.268-2.866 2.951 1.255 1.259 2.944-2.877.267.619a4.295 4.295 0 0 1 .04 3.311 4.198 4.198 0 0 1-.923 1.392 4.27 4.27 0 0 1-.743.581 4.217 4.217 0 0 1-3.812.446c-1.098 1.112-3.84 3.872-5.32 5.254a1.63 1.63 0 0 1-1.084.423zm7.938-13.047a3.32 3.32 0 0 0-1.849.557c-.213.13-.412.284-.591.458a3.321 3.321 0 0 0-.657 3.733l.135.297-.233.227c-1.738 1.697-4.269 4.22-5.485 5.504a.805.805 0 0 0 .132 1.05.911.911 0 0 0 .298.22c.1.044.209.069.319.072a.694.694 0 0 0 .45-.181c1.573-1.469 4.612-4.539 5.504-5.44l.23-.232.294.135a3.286 3.286 0 0 0 3.225-.254 3.33 3.33 0 0 0 .591-.464 3.28 3.28 0 0 0 .964-2.358c0-.215-.021-.43-.064-.642L11.43 7.125 8.879 4.578l2.515-2.59a3.286 3.286 0 0 0-.65-.06z" />
</svg>
);
default:
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path d="M13.51 4l-5-3h-1l-5 3-.49.86v6l.49.85 5 3h1l5-3 .49-.85v-6L13.51 4zm-6 9.56l-4.5-2.7V5.7l4.5 2.45v5.41zM3.27 4.7l4.74-2.84 4.74 2.84-4.74 2.59L3.27 4.7zm9.74 6.16l-4.5 2.7V8.15l4.5-2.45v5.16z" />
</svg>
);
}
}
export async function GET(request: NextRequest) {
const fontData = await fonts;
const { searchParams } = new URL(request.url);
const hasPkg = searchParams.has('pkg');
const hasKind = searchParams.has('kind');
const hasName = searchParams.has('name');
const hasMethods = searchParams.has('methods');
const hasProps = searchParams.has('props');
const hasMembers = searchParams.has('members');
const pkg = hasPkg ? resolvePackageName(searchParams.get('pkg')!) : '';
const kind = hasKind ? searchParams.get('kind')! : 'Method';
const name = hasName ? searchParams.get('name')!.slice(0, 100) : 'My default name which is super long to overflow';
const methods = hasMethods ? searchParams.get('methods') : '';
const props = hasProps ? searchParams.get('props') : '';
const members = hasMembers ? searchParams.get('members') : '';
return new ImageResponse(
(
<div
style={{
fontFamily: 'Inter',
}}
tw="flex flex-row bg-[#181818] h-full w-full p-24"
>
<div tw="flex flex-col mx-auto h-full text-white">
<div tw="flex flex-row text-4xl text-gray-400">{pkg}</div>
<div tw="flex flex-col justify-between h-full w-full pt-14">
<div tw="flex flex-row items-center max-w-full">
<span tw="mr-6">{resolveIcon(kind as keyof typeof ApiItemKind)}</span>
<h2
style={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
tw="text-[5.5rem] font-bold w-full"
>
{name}
</h2>
</div>
<div tw="flex flex-row w-full justify-between">
<div tw="flex flex-row">
{props ? (
<div tw="flex flex-row mr-12">
<span tw="mr-4">{resolveIcon('Property', 36)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{props}</span>
<span>Properties</span>
</div>
</div>
) : null}
{methods ? (
<div tw="flex flex-row mr-12">
<span tw="mr-4">{resolveIcon('Method', 36)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{methods}</span>
<span>Methods</span>
</div>
</div>
) : null}
{members ? (
<div tw="flex flex-row">
<span tw="mr-4">{resolveIcon('EnumMember', 36)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{members}</span>
<span>Members</span>
</div>
</div>
) : null}
</div>
<div tw="flex h-full items-end">
<span tw="bg-[#5865f2] text-4xl font-black relative rounded-lg py-4 px-8">discord.js</span>
</div>
</div>
</div>
</div>
</div>
),
{
width: 1_200,
height: 630,
fonts: [
{ name: 'Inter', data: fontData[0], weight: 500, style: 'normal' },
{ name: 'Inter', data: fontData[1], weight: 700, style: 'normal' },
],
debug: false,
},
);
}

View File

@@ -1,43 +0,0 @@
/* eslint-disable react/no-unknown-property */
import { ImageResponse } from '@vercel/og';
export const runtime = 'edge';
const fonts = fetch(new URL('../../../assets/fonts/Inter-Black.ttf', import.meta.url)).then(async (res) =>
res.arrayBuffer(),
);
export async function GET() {
const fontData = await fonts;
return new ImageResponse(
(
<div
style={{
fontFamily: 'Inter',
}}
tw="flex flex-row bg-[#181818] h-full w-full"
>
<div tw="mx-auto flex flex-row items-center h-full">
<div tw="flex flex-row">
<div tw="flex flex-row">
<div tw="flex flex-col font-black text-[5.5rem] text-white">
<div tw="flex flex-row">
The <span tw="bg-[#5865f2] rounded-lg py-1 px-6 ml-4">most popular</span>
</div>
<span>way to build Discord</span>
<span>bots.</span>
</div>
</div>
</div>
</div>
</div>
),
{
width: 1_200,
height: 630,
fonts: [{ name: 'Inter', data: fontData, weight: 900, style: 'normal' }],
},
);
}

View File

@@ -1,52 +0,0 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { sql } from '@vercel/postgres';
export const fetchVersions = async (packageName: string): Promise<string[]> => {
if (process.env.NEXT_PUBLIC_LOCAL_DEV === 'true' || process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return ['main'];
}
try {
const { rows } = await sql`select version from documentation where name = ${packageName} order by version desc`;
return rows.map((row) => row.version);
} catch {
return [];
}
};
export const fetchModelJSON = async (packageName: string, version: string) => {
if (process.env.NEXT_PUBLIC_LOCAL_DEV === 'true') {
try {
const res = await readFile(
join(process.cwd(), '..', '..', 'packages', packageName, 'docs', 'docs.api.json'),
'utf8',
);
return JSON.parse(res);
} catch {
return null;
}
}
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
try {
const { rows } = await sql`select url from documentation where name = ${packageName} and version = ${'main'}`;
const res = await fetch(rows[0]?.url ?? '');
return await res.json();
} catch {
return null;
}
}
try {
const { rows } = await sql`select url from documentation where name = ${packageName} and version = ${version}`;
const res = await fetch(rows[0]?.url ?? '');
return await res.json();
} catch {
return null;
}
};

View File

@@ -0,0 +1,98 @@
/* eslint-disable react/no-unknown-property */
import { ImageResponse } from 'next/og';
import { resolveKind } from '~/util/resolveNodeKind';
export const runtime = 'edge';
export const size = {
width: 1_200,
height: 630,
};
export const contentType = 'image/png';
export default async function Image({
params,
}: {
readonly params: { readonly item: string; readonly packageName: string; readonly version: string };
}) {
const normalizeItem = params.item.split(encodeURIComponent(':')).join('.').toLowerCase();
const isMainVersion = params.version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${params.packageName}/${params.version}.${normalizeItem}.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } },
);
const node = await fileContent.json();
return new ImageResponse(
(
<div tw="flex bg-[#121212] h-full w-full p-14">
<div tw="flex flex-col mx-auto h-full text-white">
<div tw="flex text-4xl text-gray-400">{params.packageName}</div>
<div tw="flex flex-col justify-between h-full w-full pt-14">
<div tw="flex items-center max-w-full">
<span tw="mr-6">{resolveKind(node.kind, 94)}</span>
<h2
style={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
tw="text-[5.5rem] font-bold w-full"
>
{node.displayName}
</h2>
</div>
<div tw="flex flex-row w-full justify-between">
<div tw="flex flex-row">
{node.members?.properties?.length ? (
<div tw="flex mr-12">
<span tw="mr-4">{resolveKind('Property', 42)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{node.members.properties.length}</span>
<span>Properties</span>
</div>
</div>
) : null}
{node.members?.events?.length ? (
<div tw="flex mr-12">
<span tw="mr-4">{resolveKind('Method', 42)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{node.members.events.length}</span>
<span>Events</span>
</div>
</div>
) : null}
{node.members?.methods?.length ? (
<div tw="flex mr-12">
<span tw="mr-4">{resolveKind('Method', 42)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{node.members.methods.length}</span>
<span>Methods</span>
</div>
</div>
) : null}
{node.members?.length ? (
<div tw="flex">
<span tw="mr-4">{resolveKind('EnumMember', 42)}</span>
<div tw="flex flex-col text-4xl">
<span tw="mb-4">{node.members.length}</span>
<span>Members</span>
</div>
</div>
) : null}
</div>
<div tw="flex h-full items-end">
<span tw="bg-[#5865f2] text-4xl font-black relative rounded-lg py-4 px-8">discord.js</span>
</div>
</div>
</div>
</div>
</div>
),
{
...size,
},
);
}

View File

@@ -0,0 +1,50 @@
// import { readFile } from 'node:fs/promises';
// import { join } from 'node:path';
// import { inspect } from 'node:util';
import type { Metadata } from 'next';
import { DocItem } from '~/components/DocItem';
export async function generateMetadata({
params,
}: {
readonly params: {
readonly item: string;
readonly packageName: string;
readonly version: string;
};
}): Promise<Metadata> {
const normalizeItem = params.item.split(encodeURIComponent(':'))[0];
return {
title: `${normalizeItem} (${params.packageName} - ${params.version})`,
};
}
export default async function Page({
params,
}: {
readonly params: { readonly item: string; readonly packageName: string; readonly version: string };
}) {
const normalizeItem = params.item.split(encodeURIComponent(':')).join('.').toLowerCase();
// const fileContent = await readFile(
// join(process.cwd(), `../../packages/${params.packageName}/docs/split/${params.version}.${normalizeItem}.api.json`),
// 'utf8',
// );
// const node = JSON.parse(fileContent);
const isMainVersion = params.version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${params.packageName}/${params.version}.${normalizeItem}.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } },
);
const node = await fileContent.json();
// console.log(inspect(node, { depth: 0 }));
return (
<main className="flex w-full flex-col gap-8 pb-12 md:pb-0">
<DocItem node={node} packageName={params.packageName} version={params.version} />
</main>
);
}

View File

@@ -0,0 +1,87 @@
// import { readFile } from 'node:fs/promises';
// import { join } from 'node:path';
// import { inspect } from 'node:util';
import type { Metadata } from 'next';
import dynamic from 'next/dynamic';
import type { PropsWithChildren } from 'react';
import { Navigation } from '~/components/Navigation';
import { OverlayScrollbarsComponent } from '~/components/OverlayScrollbars';
import { Drawer } from '~/components/ui/Drawer';
import { Footer } from '~/components/ui/Footer';
// eslint-disable-next-line promise/prefer-await-to-then
const CmdK = dynamic(async () => import('~/components/ui/CmdK').then((mod) => mod.CmdK), { ssr: false });
export async function generateMetadata({
params,
}: {
readonly params: { readonly packageName: string; readonly version: string };
}): Promise<Metadata> {
return {
title: {
template: '%s | discord.js',
default: `${params.packageName} (${params.version})`,
},
};
}
export default async function Layout({
params,
children,
}: PropsWithChildren<{ readonly params: { readonly packageName: string; readonly version: string } }>) {
// const fileContent = await readFile(
// join(process.cwd(), `../../packages/${params.packageName}/docs/split/${params.version}.dependencies.api.json`),
// 'utf8',
// );
// const dependencies = JSON.parse(fileContent);
const isMainVersion = params.version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${params.packageName}/${params.version}.dependencies.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } },
);
const parsedDependencies = await fileContent.json();
const dependencies = Object.entries<string>(parsedDependencies)
.filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor'))
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`);
// console.log(inspect(dependencies, { depth: 0 }));
return (
// eslint-disable-next-line react/no-unknown-property
<div vaul-drawer-wrapper="" className="mx-auto flex max-w-screen-xl flex-col gap-12 p-6 md:flex-row">
<div className="sticky top-6 hidden flex-shrink-0 self-start md:block">
<OverlayScrollbarsComponent
className="max-h-[calc(100dvh-48px)]"
defer
options={{
overflow: { x: 'hidden' },
scrollbars: {
autoHide: 'scroll',
autoHideDelay: 500,
autoHideSuspend: true,
clickScroll: true,
},
}}
>
<Navigation className="pr-4" packageName={params.packageName} version={params.version} />
</OverlayScrollbarsComponent>
</div>
<div className="pb-12">
{children}
<Footer />
</div>
<div className="fixed bottom-0 left-0 right-0 md:hidden">
<Drawer>
<Navigation
className="max-w-none overflow-auto p-0 lg:max-w-none"
packageName={params.packageName}
version={params.version}
drawer
/>
</Drawer>
</div>
<CmdK dependencies={dependencies} />
</div>
);
}

View File

@@ -0,0 +1,49 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import { MDXRemote } from 'next-mdx-remote/rsc';
import remarkGfm from 'remark-gfm';
import { getHighlighterCore } from 'shiki/core';
import getWasm from 'shiki/wasm';
const highlighter = await getHighlighterCore({
themes: [import('shiki/themes/vitesse-light.mjs'), import('shiki/themes/vitesse-dark.mjs')],
langs: [
import('shiki/langs/typescript.mjs'),
import('shiki/langs/javascript.mjs'),
import('shiki/langs/shellscript.mjs'),
],
loadWasm: getWasm,
});
export default async function Page({ params }: { readonly params: { readonly packageName: string } }) {
const fileContent = await readFile(
join(process.cwd(), `src/assets/readme/${params.packageName}/home-README.md`),
'utf8',
);
return (
<div className="prose prose-neutral mx-auto max-w-screen-lg dark:prose-invert">
<MDXRemote
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[
rehypeShikiFromHighlighter as any,
highlighter,
{
themes: {
light: 'vitesse-light',
dark: 'vitesse-dark',
},
},
],
],
},
}}
source={fileContent}
/>
</div>
);
}

View File

@@ -1,20 +0,0 @@
export default function Loading() {
return (
<div className="relative top-6 mx-4 min-h-xl flex flex-col items-center justify-center gap-4">
<svg
className="h-9 w-9 animate-spin text-black dark:text-white"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75 dark:opacity-100"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
/>
</svg>
<div className="text-lg font-medium">Loading...</div>
</div>
);
}

View File

@@ -1,23 +0,0 @@
'use client';
import type { Route } from 'next';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function NotFound() {
const pathname = usePathname();
const href = pathname.split('/').slice(0, -1).join('/');
return (
<div className="mx-auto max-w-lg min-h-screen flex flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">404</h1>
<h2 className="text-[2rem] md:text-[3rem]">Not found.</h2>
<Link
className="h-11 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded bg-blurple px-6 text-base text-white font-semibold leading-none no-underline outline-none active:translate-y-px focus:ring focus:ring-width-2 focus:ring-white"
href={href as Route}
>
Take me back
</Link>
</div>
);
}

View File

@@ -1,180 +0,0 @@
import type {
ApiClass,
ApiDeclaredItem,
ApiEnum,
ApiInterface,
ApiItem,
ApiItemContainerMixin,
ApiMethod,
ApiMethodSignature,
ApiProperty,
ApiPropertySignature,
ApiTypeAlias,
ApiVariable,
ApiFunction,
} from '@discordjs/api-extractor-model';
import { ApiItemKind, ApiModel, ApiPackage } from '@discordjs/api-extractor-model';
import { tryResolveSummaryText } from '@discordjs/scripts';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { fetchModelJSON } from '~/app/docAPI';
import { Class } from '~/components/model/Class';
import { Interface } from '~/components/model/Interface';
import { TypeAlias } from '~/components/model/TypeAlias';
import { Variable } from '~/components/model/Variable';
import { Enum } from '~/components/model/enum/Enum';
import { Function } from '~/components/model/function/Function';
import { OVERLOAD_SEPARATOR } from '~/util/constants';
import { fetchMember } from '~/util/fetchMember';
import { findMember } from '~/util/model';
export const revalidate = 86_400;
export interface ItemRouteParams {
item: string;
package: string;
version: string;
}
async function fetchHeadMember({ package: packageName, version, item }: ItemRouteParams) {
const modelJSON = await fetchModelJSON(packageName, version);
if (!modelJSON) {
return undefined;
}
const model = new ApiModel();
model.addMember(ApiPackage.loadFromJson(modelJSON));
const pkg = model.tryGetPackageByName(packageName);
const entry = pkg?.entryPoints[0];
if (!entry) {
return undefined;
}
const [memberName] = decodeURIComponent(item).split(OVERLOAD_SEPARATOR);
return findMember(model, packageName, memberName);
}
function resolveMemberSearchParams(packageName: string, member?: ApiItem) {
const params = new URLSearchParams({
pkg: packageName,
kind: member?.kind ?? '',
name: member?.displayName ?? '',
});
switch (member?.kind) {
case ApiItemKind.Interface:
case ApiItemKind.Class: {
const typedMember = member as ApiItemContainerMixin;
const properties = typedMember.members.filter((member) =>
[ApiItemKind.Property, ApiItemKind.PropertySignature].includes(member.kind),
) as (ApiProperty | ApiPropertySignature)[];
const methods = typedMember.members.filter((member) =>
[ApiItemKind.Method, ApiItemKind.Method].includes(member.kind),
) as (ApiMethod | ApiMethodSignature)[];
params.append('methods', methods.length.toString());
params.append('props', properties.length.toString());
break;
}
case ApiItemKind.Enum: {
const typedMember = member as ApiEnum;
params.append('members', typedMember.members.length.toString());
break;
}
default:
break;
}
return params;
}
export async function generateMetadata({ params }: { params: ItemRouteParams }) {
const member = await fetchHeadMember(params);
const name = `discord.js${member?.displayName ? ` | ${member.displayName}` : ''}`;
const ogTitle = `${params.package ?? 'discord.js'}${member?.displayName ? ` | ${member.displayName}` : ''}`;
const url = new URL('https://discordjs.dev/api/dynamic-open-graph.png');
const searchParams = resolveMemberSearchParams(params.package, member);
url.search = searchParams.toString();
const ogImage = url.toString();
let description;
if (member) {
description = tryResolveSummaryText(member as ApiDeclaredItem);
}
return {
title: name,
description: description ?? 'Discord.js API Documentation',
openGraph: {
title: ogTitle,
description: description ?? 'Discord.js API Documentation',
images: ogImage,
},
} satisfies Metadata;
}
export async function generateStaticParams({ params: { package: packageName, version } }: { params: ItemRouteParams }) {
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return [];
}
const modelJSON = await fetchModelJSON(packageName, version);
if (!modelJSON) {
return [];
}
const model = new ApiModel();
model.addMember(ApiPackage.loadFromJson(modelJSON));
const pkg = model.tryGetPackageByName(packageName);
const entry = pkg?.entryPoints[0];
if (!entry) {
return [];
}
return entry.members.map((member: ApiItem) => ({
package: packageName,
version,
item: `${member.displayName}${OVERLOAD_SEPARATOR}${member.kind}`,
}));
}
function Member({ member }: { readonly member?: ApiItem }) {
switch (member?.kind) {
case 'Class':
return <Class clazz={member as ApiClass} />;
case 'Function':
return <Function item={member as ApiFunction} />;
case 'Interface':
return <Interface item={member as ApiInterface} />;
case 'TypeAlias':
return <TypeAlias item={member as ApiTypeAlias} />;
case 'Variable':
return <Variable item={member as ApiVariable} />;
case 'Enum':
return <Enum item={member as ApiEnum} />;
default:
return <div>Cannot render that item type</div>;
}
}
export default async function Page({ params }: { params: ItemRouteParams }) {
const member = await fetchMember(params.package, params.version ?? 'main', params.item);
if (!member) {
notFound();
}
return (
<div className="relative">
<Member member={member} />
</div>
);
}

View File

@@ -1,12 +0,0 @@
'use client';
export default function Error({ error }: { readonly error: Error }) {
console.error(error);
return (
<div className="mx-auto h-full max-w-lg flex flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">500</h1>
<h2 className="text-[2rem] md:text-[3rem]">Error.</h2>
</div>
);
}

View File

@@ -1,102 +0,0 @@
import type { ApiFunction, ApiItem } from '@discordjs/api-extractor-model';
import { ApiModel, ApiPackage } from '@discordjs/api-extractor-model';
import dynamic from 'next/dynamic';
import { notFound } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { fetchModelJSON, fetchVersions } from '~/app/docAPI';
import { CmdKDialog } from '~/components/CmdK';
import { Nav } from '~/components/Nav';
import { Outline } from '~/components/Outline';
import type { SidebarSectionItemData } from '~/components/Sidebar';
import { resolveItemURI } from '~/components/documentation/util';
import { N_RECENT_VERSIONS, PACKAGES } from '~/util/constants';
import { Providers } from './providers';
const Header = dynamic(async () => import('~/components/Header'));
const Footer = dynamic(async () => import('~/components/Footer'));
interface VersionRouteParams {
package: string;
version: string;
}
export const generateStaticParams = async () => {
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return [];
}
const params: VersionRouteParams[] = [];
await Promise.all(
PACKAGES.map(async (packageName) => {
const versions = (await fetchVersions(packageName)).slice(1, N_RECENT_VERSIONS);
params.push(...versions.map((version) => ({ package: packageName, version })));
}),
);
return params;
};
const serializeIntoSidebarItemData = (item: ApiItem) => {
return {
kind: item.kind,
name: item.displayName,
href: resolveItemURI(item),
overloadIndex: 'overloadIndex' in item ? (item.overloadIndex as number) : undefined,
} as SidebarSectionItemData;
};
export default async function PackageLayout({ children, params }: PropsWithChildren<{ params: VersionRouteParams }>) {
const modelJSON = await fetchModelJSON(params.package, params.version);
if (!modelJSON) {
notFound();
}
const model = new ApiModel();
model.addMember(ApiPackage.loadFromJson(modelJSON));
const pkg = model.tryGetPackageByName(params.package);
if (!pkg) {
notFound();
}
const entry = pkg.entryPoints[0];
if (!entry) {
notFound();
}
const members = entry.members.filter((member) => {
if (member.kind !== 'Function') {
return true;
}
return (member as ApiFunction).overloadIndex === 1;
});
const versions = await fetchVersions(params.package);
return (
<Providers>
<main className="mx-auto max-w-7xl px-4 lg:max-w-full">
<Header />
<div className="relative top-6.5 mx-auto max-w-7xl gap-6 lg:max-w-full lg:flex">
<div className="lg:sticky lg:top-23 lg:h-[calc(100vh_-_105px)]">
<Nav members={members.map((member) => serializeIntoSidebarItemData(member))} versions={versions} />
</div>
<div className="relative top-4.5 mx-auto max-w-5xl min-w-xs w-full pb-10">
{children}
<Footer />
</div>
<Outline />
</div>
</main>
<CmdKDialog />
</Providers>
);
}

View File

@@ -1,34 +0,0 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { compileMDX } from 'next-mdx-remote/rsc';
import { cache } from 'react';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import { SyntaxHighlighter } from '~/components/SyntaxHighlighter';
interface VersionRouteParams {
package: string;
version: string;
}
const loadREADME = cache(async (packageName: string) => {
return readFile(join(process.cwd(), 'src', 'assets', 'readme', packageName, 'home-README.md'), 'utf8');
});
export default async function Page({ params }: { params: VersionRouteParams }) {
const readmeSource = await loadREADME(params.package);
const { content } = await compileMDX({
source: readmeSource,
// @ts-expect-error SyntaxHighlighter is assignable
components: { pre: SyntaxHighlighter },
options: {
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeSlug],
format: 'mdx',
},
},
});
return <div className="relative top-4 max-w-none prose">{content}</div>;
}

View File

@@ -1,19 +0,0 @@
'use client';
import type { PropsWithChildren } from 'react';
import { CmdKProvider } from '~/contexts/cmdK';
import { MemberProvider } from '~/contexts/member';
import { NavProvider } from '~/contexts/nav';
import { OutlineProvider } from '~/contexts/outline';
export function Providers({ children }: PropsWithChildren) {
return (
<NavProvider>
<OutlineProvider>
<MemberProvider>
<CmdKProvider>{children}</CmdKProvider>
</MemberProvider>
</OutlineProvider>
</NavProvider>
);
}

View File

@@ -1 +0,0 @@
export { default } from '~/app/loading';

View File

@@ -1,44 +0,0 @@
import { VscArrowLeft } from '@react-icons/all-files/vsc/VscArrowLeft';
import { VscArrowRight } from '@react-icons/all-files/vsc/VscArrowRight';
import { VscVersions } from '@react-icons/all-files/vsc/VscVersions';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { fetchVersions } from '~/app/docAPI';
import { buttonVariants } from '~/styles/Button';
import { PACKAGES } from '~/util/constants';
export const revalidate = 86_400;
export default async function Page({ params }: { params: { package: string } }) {
if (!PACKAGES.includes(params.package)) {
notFound();
}
const data = await fetchVersions(params.package);
return (
<div className="mx-auto min-h-screen min-w-xs flex flex-col gap-8 px-4 py-6 sm:w-md lg:px-6 lg:py-6">
<h1 className="text-2xl font-semibold">Select a version:</h1>
<div className="flex flex-col gap-4">
{data.map((version, idx) => (
<Link
className={buttonVariants({ variant: 'secondary' })}
href={`/docs/packages/${params.package}/${version}`}
key={`${version}-${idx}`}
>
<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">
<VscVersions size={25} />
<h2 className="font-semibold">{version}</h2>
</div>
<VscArrowRight size={20} />
</div>
</Link>
)) ?? null}
</div>
<Link className={buttonVariants({ className: 'place-self-center' })} href="/docs/packages">
<VscArrowLeft size={20} /> Go back
</Link>
</div>
);
}

View File

@@ -1 +0,0 @@
export { default } from '~/app/loading';

View File

@@ -1,44 +0,0 @@
import { FiExternalLink } from '@react-icons/all-files/fi/FiExternalLink';
import { VscArrowLeft } from '@react-icons/all-files/vsc/VscArrowLeft';
import { VscArrowRight } from '@react-icons/all-files/vsc/VscArrowRight';
import { VscPackage } from '@react-icons/all-files/vsc/VscPackage';
import Link from 'next/link';
import { buttonVariants } from '~/styles/Button';
import { PACKAGES } from '~/util/constants';
export default function Page() {
return (
<div className="mx-auto min-h-screen min-w-xs flex flex-col gap-8 px-4 py-6 sm:w-md lg:px-6 lg:py-6">
<h1 className="text-2xl font-semibold">Select a package:</h1>
<div className="flex flex-col gap-4">
{PACKAGES.map((pkg, idx) => (
<Link
className={buttonVariants({ variant: 'secondary' })}
href={`/docs/packages/${pkg}`}
key={`${pkg}-${idx}`}
>
<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>
<VscArrowRight size={20} />
</div>
</Link>
))}
<a className={buttonVariants({ variant: 'secondary' })} href="https://discord-api-types.dev/">
<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-api-types</h2>
</div>
<FiExternalLink size={20} />
</div>
</a>
</div>
<Link className={buttonVariants({ className: 'place-self-center' })} href="/">
<VscArrowLeft size={20} /> Go back
</Link>
</div>
);
}

View File

@@ -1,12 +0,0 @@
'use client';
export default function Error({ error }: { readonly error: Error }) {
console.error(error);
return (
<div className="mx-auto max-w-lg min-h-screen flex flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">500</h1>
<h2 className="text-[2rem] md:text-[3rem]">Error.</h2>
</div>
);
}

View File

@@ -1,26 +1,33 @@
import { Analytics } from '@vercel/analytics/react';
import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans';
import type { Metadata, Viewport } from 'next';
import type { PropsWithChildren } from 'react';
import { LocalizedStringProvider } from 'react-aria-components/i18n';
import { DESCRIPTION } from '~/util/constants';
import { inter, jetBrainsMono } from '~/util/fonts';
import { Providers } from './providers';
import '~/styles/cmdk.css';
import '~/styles/main.css';
import 'overlayscrollbars/overlayscrollbars.css';
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#f1f3f5' },
{ media: '(prefers-color-scheme: dark)', color: '#1c1c1e' },
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#121212' },
],
colorScheme: 'light dark',
};
export const metadata: Metadata = {
metadataBase: new URL(
process.env.METADATA_BASE_URL ? process.env.METADATA_BASE_URL : `http://localhost:${process.env.PORT ?? 3_000}`,
process.env.NEXT_PUBLIC_LOCAL_DEV === 'true'
? `http://localhost:${process.env.PORT ?? 3_000}`
: 'https://discord.js.org',
),
title: 'discord.js',
title: {
template: '%s | discord.js',
default: 'discord.js',
},
description: DESCRIPTION,
icons: {
other: [
@@ -66,14 +73,15 @@ export const metadata: Metadata = {
},
other: {
'msapplication-TileColor': '#1c1c1e',
'msapplication-TileColor': '#121212',
},
};
export default function RootLayout({ children }: PropsWithChildren) {
export default async function RootLayout({ children }: PropsWithChildren) {
return (
<html className={`${inter.variable} ${jetBrainsMono.variable}`} lang="en" suppressHydrationWarning>
<body className="bg-light-600 dark:bg-dark-600 dark:text-light-900">
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable} antialiased`} suppressHydrationWarning>
<body className="bg-white dark:bg-[#121212]">
<LocalizedStringProvider locale="en-US" />
<Providers>{children}</Providers>
<Analytics />
</body>

View File

@@ -1,20 +0,0 @@
export default function Loading() {
return (
<div className="mx-4 min-h-screen flex flex-col items-center justify-center gap-4">
<svg
className="h-9 w-9 animate-spin text-black dark:text-white"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75 dark:opacity-100"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
/>
</svg>
<div className="text-lg font-medium">Loading...</div>
</div>
);
}

View File

@@ -1,17 +0,0 @@
import type { Route } from 'next';
import Link from 'next/link';
export default function NotFound() {
return (
<div className="mx-auto max-w-lg min-h-screen flex flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">404</h1>
<h2 className="text-[2rem] md:text-[3rem]">Not found.</h2>
<Link
className="h-11 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded bg-blurple px-6 text-base text-white font-semibold leading-none no-underline outline-none active:translate-y-px focus:ring focus:ring-width-2 focus:ring-white"
href={'/docs' as Route}
>
Take me back
</Link>
</div>
);
}

View File

@@ -0,0 +1,36 @@
/* eslint-disable react/no-unknown-property */
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = {
width: 1_200,
height: 630,
};
export const contentType = 'image/png';
export default async function Image() {
return new ImageResponse(
(
<div tw="flex bg-[#121212] h-full w-full">
<div tw="mx-auto flex items-center h-full">
<div tw="flex">
<div tw="flex">
<div tw="flex flex-col font-black text-[5.5rem] text-white">
<div tw="flex flex-row">
The <span tw="bg-[#5865f2] rounded-lg py-1 px-6 ml-4">most popular</span>
</div>
<span>way to build Discord</span>
<span>bots.</span>
</div>
</div>
</div>
</div>
</div>
),
{
...size,
},
);
}

View File

@@ -1,51 +1,51 @@
import { FiExternalLink } from '@react-icons/all-files/fi/FiExternalLink';
import type { Route } from 'next';
import { ExternalLink } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import vercelLogo from '~/assets/powered-by-vercel.svg';
import workersLogo from '~/assets/powered-by-workers.png';
import { InstallButton } from '~/components/InstallButton';
import { buttonVariants } from '~/styles/Button';
import { InstallButton } from '~/components/ui/InstallButton';
import { DESCRIPTION } from '~/util/constants';
export default function Page() {
export default async function Page() {
return (
<div className="min-h-screen">
<div className="mx-auto max-w-6xl flex flex-col place-items-center gap-24 px-8 pb-16 pt-12 lg:min-h-[calc(100vh_-_40px)] lg:place-content-center lg:py-10">
<div className="flex flex-col place-items-center gap-10 lg:flex-row lg:gap-6">
<div className="flex flex-col place-items-center gap-10 text-center">
<h1 className="text-3xl font-black leading-tight sm:text-7xl sm:leading-tight">
<div className="mx-auto flex min-h-screen w-full max-w-screen-lg flex-col place-content-center place-items-center gap-24 px-8 pb-16 pt-12">
<div className="flex flex-col gap-10 text-center">
<h1 className="z-10 text-3xl font-black leading-tight sm:text-7xl sm:leading-tight">
The <span className="relative rounded bg-blurple px-3 py-1 text-white">most popular</span> way to build
Discord bots.
</h1>
<p className="my-6 text-neutral-700 leading-normal dark:text-neutral-300">{DESCRIPTION}</p>
<div className="flex flex-wrap place-content-center gap-4 md:flex-row">
<Link className={buttonVariants()} href={'/docs' as Route}>
<p className="z-10 leading-normal text-neutral-700 dark:text-neutral-300 md:my-6">{DESCRIPTION}</p>
<div className="flex flex-wrap place-content-center gap-4 sm:flex-wrap md:flex-row">
<Link
className="inline-flex rounded-md border border-transparent bg-blurple px-6 py-2 font-medium text-white"
href="/docs"
>
Docs
</Link>
<a
className={buttonVariants({ variant: 'secondary' })}
className="inline-flex gap-2 rounded-md border border-neutral-300 bg-white px-6 py-2 font-medium hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800"
href="https://discordjs.guide"
rel="noopener noreferrer"
target="_blank"
>
Guide <FiExternalLink />
Guide <ExternalLink aria-hidden size={20} />
</a>
<a
className={buttonVariants({ variant: 'secondary' })}
className="inline-flex gap-2 rounded-md border border-neutral-300 bg-white px-6 py-2 font-medium hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800"
href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer"
target="_blank"
>
GitHub <FiExternalLink />
GitHub <ExternalLink aria-hidden size={20} />
</a>
</div>
<InstallButton />
</div>
<InstallButton className="place-self-center" />
</div>
<div className="flex flex-col gap-4 md:flex-row">
<a
className="rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
rel="external noopener noreferrer"
target="_blank"
@@ -62,7 +62,6 @@ export default function Page() {
/>
</a>
<a
className="rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
href="https://www.cloudflare.com"
rel="external noopener noreferrer"
target="_blank"
@@ -79,6 +78,5 @@ export default function Page() {
</a>
</div>
</div>
</div>
);
}

View File

@@ -1,13 +1,24 @@
'use client';
import { Provider as JotaiProvider } from 'jotai';
import { useRouter } from 'next/navigation';
import { ThemeProvider } from 'next-themes';
import type { PropsWithChildren } from 'react';
import { RouterProvider } from 'react-aria-components';
import { useSystemThemeFallback } from '~/hooks/useSystemThemeFallback';
import { useUnregisterServiceWorker } from '~/hooks/useUnregisterServiceWorker';
export function Providers({ children }: PropsWithChildren) {
const router = useRouter();
useUnregisterServiceWorker();
useSystemThemeFallback();
return <ThemeProvider attribute="class">{children}</ThemeProvider>;
return (
// eslint-disable-next-line @typescript-eslint/unbound-method
<RouterProvider navigate={router.push}>
<JotaiProvider>
<ThemeProvider attribute="class">{children}</ThemeProvider>
</JotaiProvider>
</RouterProvider>
);
}

View File

@@ -1,9 +0,0 @@
import { FiLink } from '@react-icons/all-files/fi/FiLink';
export function Anchor({ href }: { readonly href: string }) {
return (
<a className="mr-1 inline-block rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple" href={href}>
<FiLink size={20} />
</a>
);
}

View File

@@ -1,42 +1,38 @@
import type { ApiDocumentedItem } from '@discordjs/api-extractor-model';
import { ApiAbstractMixin, ApiProtectedMixin, ApiReadonlyMixin, ApiStaticMixin } from '@discordjs/api-extractor-model';
import { AlertTriangle } from 'lucide-react';
import type { PropsWithChildren } from 'react';
export enum BadgeColor {
Danger = 'bg-red-500',
Primary = 'bg-blurple',
Warning = 'bg-yellow-500',
}
export function Badge({
children,
color = BadgeColor.Primary,
}: PropsWithChildren<{ readonly color?: BadgeColor | undefined }>) {
export function Badge({ children, className = '' }: PropsWithChildren<{ readonly className?: string }>) {
return (
<span
className={`h-5 flex flex-row place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white ${color}`}
className={`inline-flex place-items-center gap-1 rounded-full px-2 py-1 font-sans text-sm font-normal leading-none ${className}`}
>
{children}
</span>
);
}
export function Badges({ item }: { readonly item: ApiDocumentedItem }) {
const isStatic = ApiStaticMixin.isBaseClassOf(item) && item.isStatic;
const isProtected = ApiProtectedMixin.isBaseClassOf(item) && item.isProtected;
const isReadonly = ApiReadonlyMixin.isBaseClassOf(item) && item.isReadonly;
const isAbstract = ApiAbstractMixin.isBaseClassOf(item) && item.isAbstract;
const isDeprecated = Boolean(item.tsdocComment?.deprecatedBlock);
export async function Badges({ node }: { readonly node: any }) {
const isDeprecated = Boolean(node.summary?.deprecatedBlock?.length);
const isProtected = node.isProtected;
const isStatic = node.isStatic;
const isAbstract = node.isAbstract;
const isReadonly = node.isReadonly;
const isOptional = node.isOptional;
const isAny = isStatic || isProtected || isReadonly || isAbstract || isDeprecated;
const isAny = isDeprecated || isProtected || isStatic || isAbstract || isReadonly || isOptional;
return isAny ? (
<div className="flex flex-row gap-1 md:ml-7">
{isDeprecated ? <Badge color={BadgeColor.Danger}>Deprecated</Badge> : null}
{isProtected ? <Badge>Protected</Badge> : null}
{isStatic ? <Badge>Static</Badge> : null}
{isAbstract ? <Badge>Abstract</Badge> : null}
{isReadonly ? <Badge>Readonly</Badge> : null}
<div className="mb-1 flex gap-3">
{isDeprecated ? (
<Badge className="bg-red-500/20 text-red-500">
<AlertTriangle aria-hidden size={14} /> deprecated
</Badge>
) : null}
{isProtected ? <Badge className="bg-purple-500/20 text-purple-500">protected</Badge> : null}
{isStatic ? <Badge className="bg-purple-500/20 text-purple-500">static</Badge> : null}
{isAbstract ? <Badge className="bg-cyan-500/20 text-cyan-500">abstract</Badge> : null}
{isReadonly ? <Badge className="bg-purple-500/20 text-purple-500">readonly</Badge> : null}
{isOptional ? <Badge className="bg-cyan-500/20 text-cyan-500">optional</Badge> : null}
</div>
) : null;
}

View File

@@ -1,139 +0,0 @@
'use client';
import type { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscArrowRight } from '@react-icons/all-files/vsc/VscArrowRight';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import { Dialog } from 'ariakit/dialog';
import { Command } from 'cmdk';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useKey } from 'react-use';
import { useCmdK } from '~/contexts/cmdK';
import { client } from '~/util/search';
function resolveIcon(item: keyof typeof 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 <VscSymbolVariable className="shrink-0" size={25} />;
case 'Variable':
return <VscSymbolVariable className="shrink-0" size={25} />;
case 'Event':
return <VscSymbolEvent className="shrink-0" size={25} />;
default:
return <VscSymbolMethod className="shrink-0" size={25} />;
}
}
export function CmdKDialog() {
const pathname = usePathname();
const router = useRouter();
const dialog = useCmdK();
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const packageName = pathname?.split('/').slice(3, 4)[0];
const branchName = pathname?.split('/').slice(4, 5)[0];
const searchResultItems = useMemo(
() =>
searchResults?.map((item, idx) => (
<Command.Item
className="my-1 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-content-center rounded bg-transparent px-4 py-2 text-base text-black font-semibold leading-none outline-none active:translate-y-px dark:border-dark-100 active:bg-neutral-200 hover:bg-neutral-100 dark:text-white [&[aria-selected]]:ring [&[aria-selected]]:ring-width-2 [&[aria-selected]]:ring-blurple dark:active:bg-dark-200 dark:hover:bg-dark-300"
key={`${item.id}-${idx}`}
onSelect={() => {
router.push(item.path);
dialog!.setOpen(false);
}}
value={`${item.id}`}
>
<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 flex flex-col sm:w-100">
<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 sm:block dark:opacity-50">
{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(`${packageName?.replaceAll('.', '-')}-${version}`)
.search(searchString, { limit: 5 });
setSearchResults(res.hits);
};
if (search && packageName) {
void searchDoc(search, branchName?.replaceAll('.', '-') ?? 'main');
} else {
setSearchResults([]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search]);
return (
<Dialog className="fixed left-1/2 top-1/4 z-50 -translate-x-1/2" state={dialog!}>
<Command
className="max-w-xs min-w-xs border border-light-900 rounded bg-white/50 shadow backdrop-blur-md sm:max-w-lg sm:min-w-lg dark:border-dark-100 dark:bg-dark/50"
label="Command Menu"
shouldFilter={false}
>
<Command.Input
className="w-full border-0 border-b border-light-900 rounded rounded-b-0 bg-white/50 p-4 text-lg caret-blurple outline-none dark:border-dark-100 dark:bg-dark/50 placeholder:text-dark-300/75 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

@@ -1,40 +0,0 @@
import type { ReactNode } from 'react';
import { Anchor } from './Anchor';
import { SourceLink } from './documentation/SourceLink';
export interface CodeListingProps {
/**
* The value of this heading.
*/
readonly children: ReactNode;
/**
* Additional class names to apply to the root element.
*/
readonly className?: string | undefined;
/**
* The href of this heading.
*/
readonly href?: string | undefined;
/**
* The line in the source code where this part is declared
*/
readonly sourceLine?: number | undefined;
/**
* The URL of the source code of this code part
*/
readonly sourceURL?: string | undefined;
}
export function CodeHeading({ href, className, children, sourceURL, sourceLine }: CodeListingProps) {
return (
<div className="flex flex-row place-items-center justify-between gap-1">
<div
className={`flex flex-row flex-wrap place-items-center gap-1 break-all font-mono text-lg font-bold ${className}`}
>
{href ? <Anchor href={href} /> : null}
{children}
</div>
{sourceURL ? <SourceLink className="text-2xl" sourceLine={sourceLine} sourceURL={sourceURL} /> : null}
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { ParameterNode } from './ParameterNode';
import { SummaryNode } from './SummaryNode';
export async function ConstructorNode({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} />
Constructors
</h2>
<div className="flex place-content-between place-items-center">
<h3 id="constructor" className="group scroll-mt-8 break-words font-mono font-semibold">
{/* constructor({parsedContent.constructor.parametersString}) */}
<Link href="#constructor" className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block">
<LinkIcon aria-hidden size={16} />
</Link>
constructor({node.parameters?.length ? <ParameterNode node={node.parameters} version={version} /> : null})
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={node.sourceLine ? `${node.sourceURL}#L${node.sourceLine}` : node.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{node.summary?.summarySection.length ? (
<SummaryNode padding node={node.summary.summarySection} version={version} />
) : null}
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { DocNode } from './DocNode';
import { Alert } from './ui/Alert';
export async function DeprecatedNode({
deprecatedBlock,
version,
}: {
readonly deprecatedBlock: any;
readonly version: string;
}) {
return (
<Alert title="Deprecated" type="danger">
<p className="break-words">
<DocNode node={deprecatedBlock} version={version} />
</p>
</Alert>
);
}

View File

@@ -0,0 +1,134 @@
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { ConstructorNode } from './ConstructorNode';
import { DeprecatedNode } from './DeprecatedNode';
import { EnumMemberNode } from './EnumMemberNode';
import { EventNode } from './EventNode';
import { InformationNode } from './InformationNode';
import { MethodNode } from './MethodNode';
import { Outline } from './Outline';
import { OverlayScrollbarsComponent } from './OverlayScrollbars';
import { ParameterNode } from './ParameterNode';
import { PropertyNode } from './PropertyNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { SyntaxHighlighter } from './SyntaxHighlighter';
import { TypeParameterNode } from './TypeParameterNode';
import { UnionMember } from './UnionMember';
import { Tab, TabList, TabPanel, Tabs } from './ui/Tabs';
async function OverloadNode({ node, packageName, version }: { node: any; packageName: string; version: string }) {
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2">
{node.overloads.map((overload: any) => {
return (
<Tab
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
>
<span>Overload {overload.overloadIndex}</span>
</Tab>
);
})}
</TabList>
{node.overloads.map((overload: any) => {
return (
<TabPanel
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
className="flex flex-col gap-8"
>
<DocItem node={overload} packageName={packageName} version={version} />
</TabPanel>
);
})}
</Tabs>
);
}
export function DocItem({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
if (node.overloads?.length) {
return <OverloadNode node={node} packageName={packageName} version={version} />;
}
return (
<>
<InformationNode node={node} version={version} />
<OverlayScrollbarsComponent
defer
options={{
overflow: { y: 'hidden' },
scrollbars: { autoHide: 'scroll', autoHideDelay: 500, autoHideSuspend: true, clickScroll: true },
}}
className="rounded-md border border-neutral-300 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900"
>
<SyntaxHighlighter className="py-4 text-sm" lang="typescript" code={node.sourceExcerpt} />
</OverlayScrollbarsComponent>
{node.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={node.summary.deprecatedBlock} version={version} />
) : null}
{node.summary?.summarySection ? <SummaryNode node={node.summary.summarySection} version={version} /> : null}
{node.summary?.returnsBlock.length ? <ReturnNode node={node.summary.returnsBlock} version={version} /> : null}
{node.summary?.seeBlocks.length ? <SeeNode node={node.summary.seeBlocks} version={version} /> : null}
<Outline node={node} />
{node.constructor?.parametersString ? <ConstructorNode node={node.constructor} version={version} /> : null}
{node.typeParameters?.length ? (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} />
Type Parameters
</h2>
<TypeParameterNode description node={node.typeParameters} version={version} />
</div>
) : null}
{node.parameters?.length ? (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} />
Parameters
</h2>
<ParameterNode description node={node.parameters} version={version} />
</div>
) : null}
{node.members?.properties?.length ? (
<PropertyNode node={node.members.properties} packageName={packageName} version={version} />
) : null}
{node.members?.events?.length ? (
<div>
<EventNode node={node.members.events} packageName={packageName} version={version} />
</div>
) : null}
{node.members?.methods?.length ? (
<div>
<MethodNode node={node.members.methods} packageName={packageName} version={version} />
</div>
) : null}
{node.members?.length ? <EnumMemberNode node={node.members} packageName={packageName} version={version} /> : null}
{node.unionMembers?.length ? <UnionMember node={node.unionMembers} version={version} /> : null}
</>
);
}

View File

@@ -0,0 +1,44 @@
export function resolveNodeKind(kind: string) {
switch (kind) {
case 'Class':
return {
text: 'text-green-500',
background: 'bg-green-500/20',
};
case 'Interface':
return {
text: 'text-amber-500',
background: 'bg-amber-500/20',
};
case 'Function':
return {
text: 'text-blue-500',
background: 'bg-blue-500/20',
};
case 'Enum':
return {
text: 'text-rose-500',
background: 'bg-rose-500/20',
};
case 'TypeAlias':
return {
text: 'text-pink-500',
background: 'bg-pink-500/20',
};
case 'Variable':
return {
text: 'text-purple-500',
background: 'bg-purple-500/20',
};
default:
return {
text: 'text-gray-500',
background: 'bg-gray-500/20',
};
}
}
export async function DocKind({ background = false, node }: { readonly background?: boolean; readonly node: any }) {
const kind = resolveNodeKind(node.kind);
return <span className={background ? `${kind.background} ${kind.text}` : kind.text}>{node.kind.toLowerCase()}</span>;
}

View File

@@ -0,0 +1,72 @@
import Link from 'next/link';
import { OverlayScrollbarsComponent } from './OverlayScrollbars';
import { SyntaxHighlighter } from './SyntaxHighlighter';
export async function DocNode({ node, version }: { readonly node?: any; readonly version: string }) {
const createNode = (node: any, idx: number) => {
switch (node.kind) {
case 'PlainText':
return <span key={`${node.text}-${idx}`}>{node.text}</span>;
case 'LinkTag': {
if (node.resolvedPackage) {
return (
<Link
key={`${node.text}-${idx}`}
className="font-mono text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={`/docs/packages/${node.resolvedPackage.packageName}/${version}/${node.uri}`}
>
{node.text}
</Link>
);
}
if (node.uri) {
return (
<a
key={`${node.text}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={node.uri}
rel="external noreferrer noopener"
target="_blank"
>
{node.text}
</a>
);
}
return <span key={`${node.text}-${idx}`}>{node.text}</span>;
}
case 'CodeSpan':
return (
<code key={`${node.text}-${idx}`} className="font-mono text-sm">
{node.text}
</code>
);
case 'FencedCode': {
const { language, text } = node;
return (
<OverlayScrollbarsComponent
defer
options={{
overflow: { y: 'hidden' },
scrollbars: { autoHide: 'scroll', autoHideDelay: 500, autoHideSuspend: true, clickScroll: true },
}}
className="my-4 rounded-md border border-neutral-300 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900"
>
<SyntaxHighlighter className="py-4 text-sm " lang={language} code={text} />
</OverlayScrollbarsComponent>
);
}
case 'SoftBreak':
return null;
default:
return null;
}
};
return node?.map(createNode) ?? null;
}

View File

@@ -1,9 +0,0 @@
import type { PropsWithChildren } from 'react';
export function DocumentationLink({ children, href }: PropsWithChildren<{ readonly href: string }>) {
return (
<a className="text-blurple" href={href} rel="external noreferrer noopener" target="_blank">
{children}
</a>
);
}

View File

@@ -0,0 +1,109 @@
import { VscSymbolEnumMember } from '@react-icons/all-files/vsc/VscSymbolEnumMember';
import { Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode';
import { ExcerptNode } from './ExcerptNode';
import { InheritedFromNode } from './InheritedFromNode';
import { ParameterNode } from './ParameterNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
export async function EnumMemberNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolEnumMember aria-hidden className="flex-shrink-0" size={24} />
Members
</h2>
<div className="flex flex-col gap-8">
{node.map((enumMember: any, idx: number) => {
return (
<Fragment key={`${enumMember.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3 id={enumMember.displayName} className="group scroll-mt-8 break-words font-mono font-semibold">
<Badges node={enumMember} />
<span>
<Link
href={`#${enumMember.displayName}`}
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block"
>
<LinkIcon aria-hidden size={16} />
</Link>
{enumMember.displayName}
{enumMember.parameters?.length ? (
<ParameterNode node={enumMember.parameters} version={version} />
) : null}
{enumMember.initializerExcerpt ? (
<>
{' = '}
<ExcerptNode node={enumMember.initializerExcerpt} version={version} />
</>
) : null}
</span>
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={
enumMember.sourceLine ? `${enumMember.sourceURL}#L${enumMember.sourceLine}` : enumMember.sourceURL
}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{enumMember.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={enumMember.summary.deprecatedBlock} version={version} />
) : null}
{enumMember.summary?.summarySection.length ? (
<SummaryNode padding node={enumMember.summary.summarySection} version={version} />
) : null}
{enumMember.summary?.exampleBlocks.length ? (
<ExampleNode node={enumMember.summary.exampleBlocks} version={version} />
) : null}
{enumMember.summary?.returnsBlock.length ? (
<ReturnNode padding node={enumMember.summary.returnsBlock} version={version} />
) : null}
{enumMember.inheritedFrom ? (
<InheritedFromNode node={enumMember.inheritedFrom} packageName={packageName} version={version} />
) : null}
{enumMember.summary?.seeBlocks.length ? (
<SeeNode padding node={enumMember.summary.seeBlocks} version={version} />
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</Fragment>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,170 @@
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { ChevronDown, ChevronUp, Code2 } from 'lucide-react';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode';
import { InheritedFromNode } from './InheritedFromNode';
import { ParameterNode } from './ParameterNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { TypeParameterNode } from './TypeParameterNode';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { Tab, TabList, TabPanel, Tabs } from './ui/Tabs';
async function EventBodyNode({
event,
packageName,
version,
overload = false,
}: {
readonly event: any;
readonly overload?: boolean;
readonly packageName: string;
readonly version: string;
}) {
return (
<>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3
id={event.displayName}
className={`${overload ? 'scroll-mt-16' : 'scroll-mt-8'} break-words font-mono font-semibold`}
>
<Badges node={event} /> {event.displayName}
{event.typeParameters?.length ? (
<>
{'<'}
<TypeParameterNode node={event.typeParameters} version={version} />
{'>'}
</>
) : null}
({event.parameters?.length ? <ParameterNode node={event.parameters} version={version} /> : null})
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={event.sourceLine ? `${event.sourceURL}#L${event.sourceLine}` : event.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{event.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={event.summary.deprecatedBlock} version={version} />
) : null}
{event.summary?.summarySection.length ? (
<SummaryNode padding node={event.summary.summarySection} version={version} />
) : null}
{event.summary?.exampleBlocks.length ? (
<ExampleNode node={event.summary.exampleBlocks} version={version} />
) : null}
{event.summary?.returnsBlock.length ? (
<ReturnNode padding node={event.summary.returnsBlock} version={version} />
) : null}
{event.inheritedFrom ? (
<InheritedFromNode node={event.inheritedFrom} packageName={packageName} version={version} />
) : null}
{event.summary?.seeBlocks.length ? <SeeNode padding node={event.summary.seeBlocks} version={version} /> : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</>
);
}
async function OverloadNode({
event,
packageName,
version,
}: {
readonly event: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2">
{event.overloads.map((overload: any) => {
return (
<Tab
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
>
<span>Overload {overload.overloadIndex}</span>
</Tab>
);
})}
</TabList>
{event.overloads.map((overload: any) => {
return (
<TabPanel
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
className="flex flex-col gap-8"
>
<EventBodyNode overload event={overload} packageName={packageName} version={version} />
</TabPanel>
);
})}
</Tabs>
);
}
export async function EventNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} /> Events
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-8">
{node.map((event: any) => {
return event.overloads?.length ? (
<OverloadNode
key={`${event.displayName}-${event.overloadIndex}`}
event={event}
packageName={packageName}
version={version}
/>
) : (
<EventBodyNode
key={`${event.displayName}-${event.overloadIndex}`}
event={event}
packageName={packageName}
version={version}
/>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -0,0 +1,10 @@
import { DocNode } from './DocNode';
export async function ExampleNode({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="break-words pl-4">
<span className="font-semibold">Examples:</span>
<DocNode node={node} version={version} />
</div>
);
}

View File

@@ -0,0 +1,66 @@
import Link from 'next/link';
import { Fragment } from 'react';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
export async function ExcerptNode({ node, version }: { readonly node?: any; readonly version: string }) {
const createExcerpt = (excerpts: any) => {
const excerpt = Array.isArray(excerpts) ? excerpts : excerpts.excerpts ?? [excerpts];
return (
<span
className={
excerpts?.type === 'Extends' || excerpts?.type === 'Implements'
? 'after:content-[",_"] last-of-type:after:content-none'
: ''
}
>
{excerpt.map((excerpt: any, idx: number) => {
if (excerpt.resolvedItem) {
return (
<Link
key={`${excerpt.resolvedItem.displayName}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={`/docs/packages/${excerpt.resolvedItem.packageName}/${version}/${excerpt.resolvedItem.uri}`}
>
{excerpt.text}
</Link>
);
}
if (excerpt.href) {
return (
<a
key={`${excerpt.text}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={excerpt.href}
rel="external noreferrer noopener"
target="_blank"
>
{excerpt.text}
</a>
);
}
if (excerpt.text in BuiltinDocumentationLinks) {
const href = BuiltinDocumentationLinks[excerpt.text as keyof typeof BuiltinDocumentationLinks];
return (
<a
key={`${excerpt.text}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={href}
rel="external noreferrer noopener"
target="_blank"
>
{excerpt.text}
</a>
);
}
return <Fragment key={`${excerpt.text}-${idx}`}>{excerpt.text}</Fragment>;
})}
</span>
);
};
return node?.map(createExcerpt) ?? null;
}

View File

@@ -1,86 +0,0 @@
import type { ApiPackage, Excerpt } from '@discordjs/api-extractor-model';
import { ExcerptTokenKind } from '@discordjs/api-extractor-model';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
import { DISCORD_API_TYPES_DOCS_URL } from '~/util/constants';
import { DocumentationLink } from './DocumentationLink';
import { ItemLink } from './ItemLink';
import { resolveCanonicalReference, resolveItemURI } from './documentation/util';
export interface ExcerptTextProps {
/**
* The package this excerpt is referenced from.
*/
readonly apiPackage: ApiPackage;
/**
* The tokens to render.
*/
readonly excerpt: Excerpt;
}
/**
* A component that renders excerpt tokens from an api item.
*/
export function ExcerptText({ excerpt, apiPackage }: ExcerptTextProps) {
return (
<span>
{excerpt.spannedTokens.map((token, idx) => {
if (token.kind === ExcerptTokenKind.Reference) {
if (token.text in BuiltinDocumentationLinks) {
const href = BuiltinDocumentationLinks[token.text as keyof typeof BuiltinDocumentationLinks];
return (
<DocumentationLink key={`${token.text}-${idx}`} href={href}>
{token.text}
</DocumentationLink>
);
}
const source = token.canonicalReference?.source;
const symbol = token.canonicalReference?.symbol;
if (source && 'packageName' in source && source.packageName === 'discord-api-types' && symbol) {
const { meaning, componentPath: path } = symbol;
let href = DISCORD_API_TYPES_DOCS_URL;
// dapi-types doesn't have routes for class members
// so we can assume this member is for an enum
if (meaning === 'member' && path && 'parent' in path) {
href += `/enum/${path.parent}#${path.component}`;
} else if (meaning === 'type' || meaning === 'var') {
href += `#${token.text}`;
} else {
href += `/${meaning}/${token.text}`;
}
return (
<DocumentationLink key={`${token.text}-${idx}`} href={href}>
{token.text}
</DocumentationLink>
);
}
const resolved = token.canonicalReference
? resolveCanonicalReference(token.canonicalReference, apiPackage)
: null;
if (!resolved) {
return token.text;
}
return (
<ItemLink
className="text-blurple"
itemURI={resolveItemURI(resolved.item)}
key={`${resolved.item.displayName}-${resolved.item.containerKey}-${idx}`}
packageName={resolved.package}
version={resolved.version}
>
{token.text}
</ItemLink>
);
}
return token.text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, '');
})}
</span>
);
}

View File

@@ -1,116 +0,0 @@
'use client';
import { FiCommand } from '@react-icons/all-files/fi/FiCommand';
import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
import { VscMenu } from '@react-icons/all-files/vsc/VscMenu';
import { VscSearch } from '@react-icons/all-files/vsc/VscSearch';
import { Button } from 'ariakit/button';
import type { Route } from 'next';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Fragment, useMemo } from 'react';
import { useCmdK } from '~/contexts/cmdK';
import { useNav } from '~/contexts/nav';
const ThemeSwitcher = dynamic(async () => import('./ThemeSwitcher'));
export default function Header() {
const pathname = usePathname();
const { setOpened } = useNav();
const dialog = useCmdK();
const pathElements = useMemo(
() =>
pathname
.split('/')
.slice(1)
.map((path, idx, original) => (
<Link
className="rounded outline-none hover:underline focus:ring focus:ring-width-2 focus:ring-blurple"
href={`/${original.slice(0, idx + 1).join('/')}` as Route}
key={`${path}-${idx}`}
>
{path}
</Link>
)),
[pathname],
);
const breadcrumbs = useMemo(
() =>
pathElements.flatMap((el, idx, array) => {
if (idx === 0) {
return (
<Fragment key={`${el.key}-${idx}`}>
<div className="mx-2">/</div>
<div>{el}</div>
<div className="mx-2">/</div>
</Fragment>
);
}
if (idx !== array.length - 1) {
return (
<Fragment key={`${el.key}-${idx}`}>
<div>{el}</div>
<div className="mx-2">/</div>
</Fragment>
);
}
return <div key={`${el.key}-${idx}`}>{el}</div>;
}),
[pathElements],
);
return (
<header className="sticky top-4 z-20 border border-light-900 rounded-md bg-white/75 shadow backdrop-blur-md dark:border-dark-100 dark:bg-dark-600/75">
<div className="block h-16 px-6">
<div className="h-full flex flex-row place-content-between place-items-center gap-8">
<Button
aria-label="Menu"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none lg:hidden active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => setOpened((open) => !open)}
>
<VscMenu size={24} />
</Button>
<div className="hidden lg:flex lg:grow lg:flex-row lg:overflow-hidden">{breadcrumbs}</div>
<Button
as="div"
className="hidden w-56 grow rounded bg-white px-4 py-2.5 outline-none md:block sm:grow-0 dark:bg-dark-800 focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => dialog?.toggle()}
>
<div className="flex flex-row place-items-center gap-4 md:justify-between">
<VscSearch size={18} />
<span className="opacity-65">Search...</span>
<div className="hidden md:flex md:flex-row md:place-items-center md:gap-2 md:opacity-65">
<FiCommand size={18} /> K
</div>
</div>
</Button>
<div className="flex flex-row place-items-center gap-4">
<Button
as="div"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none md:hidden active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => dialog?.toggle()}
>
<VscSearch size={24} />
</Button>
<Button
aria-label="GitHub"
as="a"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded rounded-full bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer"
target="_blank"
>
<VscGithubInverted size={24} />
</Button>
<ThemeSwitcher />
</div>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,33 @@
import { FileCode2 } from 'lucide-react';
import { Badges } from './Badges';
import { DocKind } from './DocKind';
import { InheritanceNode } from './InheritanceNode';
export async function InformationNode({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="flex place-content-between place-items-center">
<div className="flex flex-col gap-1">
<h1 className="text-xl">
<DocKind node={node} /> <span className="break-words font-bold">{node.displayName}</span>
</h1>
{node.implements ? <InheritanceNode text="implements" node={node.implements} version={version} /> : null}
{node.extends ? <InheritanceNode text="extends" node={node.extends} version={version} /> : null}
<Badges node={node} />
</div>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={node.sourceLine ? `${node.sourceURL}#L${node.sourceLine}` : node.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<FileCode2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { ExcerptNode } from './ExcerptNode';
export async function InheritanceNode({
text,
node,
version,
}: {
readonly node: any;
readonly text: string;
readonly version: string;
}) {
return (
<div>
<h2 className="inline-block min-w-min text-sm italic text-neutral-500 dark:text-neutral-400">{text}</h2>{' '}
<span className="break-words font-mono text-sm">
<ExcerptNode node={node} version={version} />
</span>
</div>
);
}

View File

@@ -1,17 +0,0 @@
import type { ApiDeclaredItem } from '@discordjs/api-extractor-model';
import { ItemLink } from './ItemLink';
import { resolveItemURI } from './documentation/util';
export function InheritanceText({ parent }: { readonly parent: ApiDeclaredItem }) {
return (
<span className="font-semibold">
Inherited from{' '}
<ItemLink
className="rounded text-blurple font-mono outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
itemURI={resolveItemURI(parent)}
>
{parent.displayName}
</ItemLink>
</span>
);
}

View File

@@ -0,0 +1,23 @@
import Link from 'next/link';
export async function InheritedFromNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<p className="break-words pl-4">
<span className="font-semibold">Inherited from:</span>{' '}
<Link
className="font-mono text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={`/docs/packages/${packageName}/${version}/${node}`}
>
{node.slice(0, node.indexOf(':'))}
</Link>
</p>
);
}

View File

@@ -1,38 +0,0 @@
'use client';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { FiCopy } from '@react-icons/all-files/fi/FiCopy';
import { useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { buttonVariants } from '~/styles/Button';
export function InstallButton() {
const [interacted, setInteracted] = useState(false);
const [state, copyToClipboard] = useCopyToClipboard();
useEffect(() => {
const timer = setTimeout(() => setInteracted(false), 2_000);
return () => clearTimeout(timer);
}, [interacted]);
return (
<button
className={buttonVariants({
variant: 'secondary',
className: 'cursor-copy font-mono',
})}
onClick={() => {
setInteracted(true);
copyToClipboard('npm install discord.js');
}}
type="button"
>
<span className="text-blurple font-semibold">{'>'}</span> npm install discord.js{' '}
{state.value && interacted ? (
<FiCheck className="ml-1 inline-block text-green-500" />
) : (
<FiCopy className="ml-1 inline-block" />
)}
</button>
);
}

View File

@@ -1,48 +0,0 @@
'use client';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { useCurrentPathMeta } from '~/hooks/useCurrentPathMeta';
export interface ItemLinkProps<Route extends string> extends Omit<LinkProps<Route>, 'href'> {
readonly className?: string;
/**
* The URI of the api item to link to. (e.g. `/RestManager`)
*/
readonly itemURI: string;
/**
* The name of the package the item belongs to.
*/
readonly packageName?: string | undefined;
// TODO: This needs to be properly typed above but monkey-patching it for now.
readonly title?: string | undefined;
/**
* The version of the package the item belongs to.
*/
readonly version?: string | undefined;
}
/**
* A component that renders a link to an api item.
*
* @remarks
* This component only needs the relative path to the item, and will automatically
* generate the full path to the item client-side.
*/
export function ItemLink<Route extends string>(props: PropsWithChildren<ItemLinkProps<Route>>) {
const pathname = usePathname();
const { packageName, version } = useCurrentPathMeta();
if (!pathname) {
throw new Error('ItemLink must be used inside a Next.js page. (e.g. /docs/packages/foo/main)');
}
const { itemURI, packageName: pkgName, version: pkgVersion, ...linkProps } = props;
return <Link {...linkProps} href={`/docs/packages/${pkgName ?? packageName}/${pkgVersion ?? version}/${itemURI}`} />;
}

View File

@@ -0,0 +1,180 @@
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode';
import { ExcerptNode } from './ExcerptNode';
import { InheritedFromNode } from './InheritedFromNode';
import { ParameterNode } from './ParameterNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { TypeParameterNode } from './TypeParameterNode';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { Tab, TabList, TabPanel, Tabs } from './ui/Tabs';
async function MethodBodyNode({
method,
packageName,
version,
overload = false,
}: {
readonly method: any;
readonly overload?: boolean;
readonly packageName: string;
readonly version: string;
}) {
return (
<>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3
id={method.displayName}
className={`${overload ? 'scroll-mt-16' : 'scroll-mt-8'} group break-words font-mono font-semibold`}
>
<Badges node={method} /> {method.displayName}
<span>
<Link href={`#${method.displayName}`} className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block">
<LinkIcon aria-hidden size={16} />
</Link>
{method.typeParameters?.length ? (
<>
{'<'}
<TypeParameterNode node={method.typeParameters} version={version} />
{'>'}
</>
) : null}
({method.parameters?.length ? <ParameterNode node={method.parameters} version={version} /> : null}
) : <ExcerptNode node={method.returnTypeExcerpt} version={version} />
</span>
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={method.sourceLine ? `${method.sourceURL}#L${method.sourceLine}` : method.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{method.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={method.summary.deprecatedBlock} version={version} />
) : null}
{method.summary?.summarySection.length ? (
<SummaryNode padding node={method.summary.summarySection} version={version} />
) : null}
{method.summary?.exampleBlocks.length ? (
<ExampleNode node={method.summary.exampleBlocks} version={version} />
) : null}
{method.summary?.returnsBlock.length ? (
<ReturnNode padding node={method.summary.returnsBlock} version={version} />
) : null}
{method.inheritedFrom ? (
<InheritedFromNode node={method.inheritedFrom} packageName={packageName} version={version} />
) : null}
{method.summary?.seeBlocks.length ? (
<SeeNode padding node={method.summary.seeBlocks} version={version} />
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</>
);
}
async function OverloadNode({
method,
packageName,
version,
}: {
readonly method: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2">
{method.overloads.map((overload: any) => {
return (
<Tab
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
>
<span>Overload {overload.overloadIndex}</span>
</Tab>
);
})}
</TabList>
{method.overloads.map((overload: any) => {
return (
<TabPanel
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
className="flex flex-col gap-8"
>
<MethodBodyNode overload method={overload} packageName={packageName} version={version} />
</TabPanel>
);
})}
</Tabs>
);
}
export async function MethodNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} /> Methods
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-8">
{node.map((method: any) => {
return method.overloads?.length ? (
<OverloadNode
key={`${method.displayName}-${method.overloadIndex}`}
method={method}
packageName={packageName}
version={version}
/>
) : (
<MethodBodyNode
key={`${method.displayName}-${method.overloadIndex}`}
method={method}
packageName={packageName}
version={version}
/>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -1,47 +0,0 @@
'use client';
import dynamic from 'next/dynamic';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { useNav } from '~/contexts/nav';
import { Sidebar } from './Sidebar';
import type { SidebarSectionItemData } from './Sidebar';
const PackageSelect = dynamic(async () => import('./PackageSelect'));
const VersionSelect = dynamic(async () => import('./VersionSelect'));
export function Nav({
members,
versions,
}: {
readonly members: SidebarSectionItemData[];
readonly versions: string[];
}) {
const { opened } = useNav();
return (
<nav
className={`dark:bg-dark-600/75 dark:border-dark-100 border-light-900 top-22 fixed bottom-4 left-4 right-4 z-20 mx-auto max-w-5xl rounded-md border bg-white/75 shadow backdrop-blur-md ${
opened ? 'block' : 'hidden'
} lg:min-w-xs lg:sticky lg:block lg:h-full lg:w-full lg:max-w-xs`}
>
<Scrollbars
autoHide
className="[&>div]:overscroll-none"
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="z-30 rounded bg-light-900 dark:bg-dark-100" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute bottom-0.5 right-0.5 top-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-col gap-4">
<PackageSelect />
<VersionSelect versions={versions} />
</div>
<Sidebar members={members} />
</div>
</Scrollbars>
</nav>
);
}

View File

@@ -0,0 +1,227 @@
import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
import { ChevronDown, ChevronUp } from 'lucide-react';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { fetchVersions } from '~/util/fetchVersions';
import { resolveNodeKind } from './DocKind';
import { NavigationItem } from './NavigationItem';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { PackageSelect } from './ui/PackageSelect';
import { SearchButton } from './ui/SearchButton';
import { VersionSelect } from './ui/VersionSelect';
// eslint-disable-next-line promise/prefer-await-to-then
const ThemeSwitch = dynamic(async () => import('~/components/ui/ThemeSwitch').then((mod) => mod.ThemeSwitch), {
ssr: false,
});
export async function Navigation({
className = '',
packageName,
version,
drawer = false,
}: {
readonly className?: string;
readonly drawer?: boolean;
readonly packageName: string;
readonly version: string;
}) {
const isMainVersion = version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.sitemap.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } },
);
const node = await fileContent.json();
const versions = await fetchVersions(packageName);
const groupedNodes = node.reduce((acc: any, node: any) => {
(acc[node.kind.toLowerCase()] ||= []).push(node);
return acc;
}, {});
return (
<aside className={`flex min-w-52 max-w-52 flex-col gap-2 lg:min-w-72 lg:max-w-72 ${className}`}>
<div
className={`sticky top-0 flex flex-col gap-4 pb-4 ${drawer ? 'bg-neutral-100 dark:bg-neutral-900' : 'bg-white dark:bg-[#121212]'}`}
>
<div className="flex flex-col gap-2 pt-px">
<div className="flex place-content-between place-items-center p-1">
<Link href={`/docs/packages/${packageName}/${version}`} className="text-xl font-bold">
{packageName}
</Link>
<div className="flex gap-2">
<Link
aria-label="GitHub"
className="rounded-full"
href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer"
target="_blank"
>
<VscGithubInverted aria-hidden size={24} />
</Link>
<ThemeSwitch />
</div>
</div>
<PackageSelect packageName={packageName} />
{/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */}
<VersionSelect packageName={packageName} version={version} versions={versions} />
</div>
<SearchButton />
</div>
<nav className="flex flex-col gap-4">
{groupedNodes.class?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Classes</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.class.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.function?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Functions</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.function.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.enum?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Enums</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.enum.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.interface?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Interfaces</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.interface.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.typealias?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Types</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.typealias.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.variable?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Variables</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.variable.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
</nav>
</aside>
);
}

View File

@@ -0,0 +1,34 @@
'use client';
import { useSetAtom } from 'jotai';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { isDrawerOpenAtom } from '~/stores/drawer';
export function NavigationItem({
node,
packageName,
version,
children,
}: PropsWithChildren<{
readonly node: any;
readonly packageName: string;
readonly version: string;
}>) {
const pathname = usePathname();
const setDrawerOpen = useSetAtom(isDrawerOpenAtom);
const href = `/docs/packages/${packageName}/${version}/${node.href}`;
return (
<Link
className={`truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1 ${pathname === href ? 'bg-neutral-200 font-medium text-blurple dark:bg-neutral-800' : ''}`}
href={href}
title={node.name}
onClick={() => setDrawerOpen(false)}
>
{children}
</Link>
);
}

View File

@@ -1,32 +1,135 @@
'use client';
import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { ChevronDown, ChevronUp } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { useOutline } from '~/contexts/outline';
import { Scrollbars } from './Scrollbars';
import { TableOfContentItems } from './TableOfContentItems';
export async function Outline({ node }: { readonly node: any }) {
const hasAny = node.members?.properties?.length || node.members?.events?.length || node.members?.methods?.length;
export function Outline() {
const { members } = useOutline();
return hasAny ? (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscListSelection aria-hidden className="flex-shrink-0" size={24} /> Table of contents
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
if (!members) {
return null;
}
<CollapsibleContent>
<div className="flex flex-col gap-8">
<div className="grid gap-2 sm:grid-cols-2">
{node.members?.properties?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} />
Properties
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-2 px-4">
{node.members.properties.map((property: any, idx: number) => {
return (
<div className="lg:sticky lg:top-23 lg:h-[calc(100vh_-_105px)]">
<aside className="fixed bottom-4 left-4 right-4 top-22 z-20 mx-auto hidden max-w-5xl border border-light-900 rounded-md bg-white/75 shadow backdrop-blur-md lg:sticky lg:block lg:h-full lg:max-w-xs lg:min-w-xs lg:w-full dark:border-dark-100 dark:bg-dark-600/75">
<Scrollbars
autoHide
className="[&>div]:overscroll-none"
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="z-30 rounded bg-light-900 dark:bg-dark-100" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute bottom-0.5 right-0.5 top-0.5 z-30 w-1.5 rounded" />
)}
universal
<Fragment key={`${property.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<Link
href={`#${property.displayName}`}
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1"
>
<TableOfContentItems serializedMembers={members} />
</Scrollbars>
</aside>
{property.displayName}
</Link>
</div>
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{node.members?.events?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} />
Events
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-2 px-4">
{node.members.events.map((event: any, idx: number) => {
return (
<Fragment key={`${event.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<Link
href={`#${event.displayName}`}
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1"
>
{event.displayName}
</Link>
</div>
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{node.members?.methods?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} />
Methods
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-2 px-4">
{node.members.methods.map((method: any, idx: number) => {
return (
<Fragment key={`${method.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<Link
href={`#${method.displayName}`}
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1"
>
{method.displayName}
</Link>
</div>
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
</CollapsibleContent>
</Collapsible>
) : null;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { OverlayScrollbars, ClickScrollPlugin } from 'overlayscrollbars';
OverlayScrollbars.plugin(ClickScrollPlugin);
export { OverlayScrollbarsComponent } from 'overlayscrollbars-react';

View File

@@ -1,98 +0,0 @@
'use client';
import { VscChevronDown } from '@react-icons/all-files/vsc/VscChevronDown';
import { VscVersions } from '@react-icons/all-files/vsc/VscVersions';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import type { PropsWithChildren, ReactNode } from 'react';
import { useCallback, useMemo, useState, useEffect } from 'react';
export interface OverloadSwitcherProps {
methodName: string;
overloads: ReactNode[];
}
export default function OverloadSwitcher({
methodName,
overloads,
children,
}: PropsWithChildren<{
readonly methodName: string;
readonly overloads: ReactNode[];
}>) {
const [hash, setHash] = useState(() => (typeof window === 'undefined' ? '' : window.location.hash));
const hashChangeHandler = useCallback(() => {
setHash(window.location.hash);
}, []);
const [overloadIndex, setOverloadIndex] = useState(1);
const overloadedNode = overloads[overloadIndex - 1]!;
const menu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
useEffect(() => {
window.addEventListener('hashchange', hashChangeHandler);
return () => {
window.removeEventListener('hashchange', hashChangeHandler);
};
});
useEffect(() => {
if (hash) {
const elementId = hash.replace('#', '');
const [name, idx] = elementId.split(':');
if (name && methodName === name) {
if (idx) {
const hashOverload = Number.parseInt(idx, 10);
const resolvedOverload = Math.max(Math.min(hashOverload, overloads.length), 1);
setOverloadIndex(Number.isNaN(resolvedOverload) ? 1 : resolvedOverload);
}
const element = document.querySelector(`[id^='${name}']`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
}
}, [hash, methodName, overloads.length]);
const menuItems = useMemo(
() =>
overloads.map((_, idx) => (
<MenuItem
className="my-0.5 cursor-pointer rounded bg-white p-3 text-sm outline-none active:bg-light-800 dark:bg-dark-600 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-500"
key={idx}
onClick={() => setOverloadIndex(idx + 1)}
>
{`Overload ${idx + 1}`}
</MenuItem>
)),
[overloads],
);
return (
<div className="flex flex-col place-items-start gap-2">
<MenuButton
className="mb-2 rounded bg-white p-3 outline-none active:bg-light-900 dark:bg-dark-400 hover:bg-light-800 focus:ring focus:ring-width-2 focus:ring-blurple md:-ml-2 dark:active:bg-dark-200 dark:hover:bg-dark-300"
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 ${overloads.length}`}
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${menu.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="z-20 flex flex-col border border-light-800 rounded bg-white p-1 outline-none dark:border-dark-100 dark:bg-dark-600 focus:ring focus:ring-width-2 focus:ring-blurple"
state={menu}
>
{menuItems}
</Menu>
{children}
{overloadedNode}
</div>
);
}

View File

@@ -1,63 +0,0 @@
'use client';
import { VscChevronDown } from '@react-icons/all-files/vsc/VscChevronDown';
import { VscPackage } from '@react-icons/all-files/vsc/VscPackage';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useMemo } from 'react';
import { PACKAGES } from '~/util/constants';
export default function PackageSelect() {
const pathname = usePathname();
const packageName = pathname?.split('/').slice(3, 4)[0];
const packageMenu = useMenuState({
gutter: 8,
sameWidth: true,
fitViewport: true,
});
const packageMenuItems = useMemo(
() =>
PACKAGES.map((pkg, idx) => (
<Link href={`/docs/packages/${pkg}/main`} key={`${pkg}-${idx}`}>
<MenuItem
className="my-0.5 rounded bg-white p-3 text-sm outline-none active:bg-light-800 dark:bg-dark-600 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-500"
id={pkg}
onClick={() => packageMenu.setOpen(false)}
state={packageMenu}
>
{pkg}
</MenuItem>
</Link>
)),
[packageMenu],
);
return (
<>
<MenuButton
className="rounded bg-light-600 p-3 outline-none active:bg-light-800 dark:bg-dark-400 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-300"
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="z-20 flex flex-col border border-light-800 rounded bg-white p-1 outline-none dark:border-dark-100 dark:bg-dark-600 focus:ring focus:ring-width-2 focus:ring-blurple"
state={packageMenu}
>
{packageMenuItems}
</Menu>
</>
);
}

View File

@@ -1,10 +0,0 @@
import type { PropsWithChildren } from 'react';
export function Panel({ children }: PropsWithChildren) {
return (
<>
{children}
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</>
);
}

View File

@@ -0,0 +1,49 @@
import { LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DocNode } from './DocNode';
import { ExcerptNode } from './ExcerptNode';
export async function ParameterNode({
description = false,
node,
version,
}: {
readonly description?: boolean;
readonly node: any;
readonly version: string;
}) {
return (
<div className={`${description ? 'flex flex-col gap-8' : 'inline'}`}>
{node.map((parameter: any, idx: number) => {
return (
<Fragment key={`${parameter.name}-${idx}`}>
<div className={description ? 'group' : 'inline after:content-[",_"] last-of-type:after:content-none'}>
<span className="font-mono font-semibold">
{description ? (
<Link href={`#${parameter.name}`} className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block">
<LinkIcon aria-hidden size={16} />
</Link>
) : null}
{description ? <Badges node={parameter} /> : null}
{parameter.name}
{parameter.isOptional ? '?' : ''}: <ExcerptNode node={parameter.typeExcerpt} version={version} />
</span>
{description && parameter.description?.length ? (
<div className="mt-4 pl-4">
<DocNode node={parameter.description} version={version} />
</div>
) : null}
</div>
</Fragment>
);
})}
{description ? (
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
) : null}
</div>
);
}

View File

@@ -1,32 +0,0 @@
import type { ApiDocumentedItem, ApiParameterListMixin } from '@discordjs/api-extractor-model';
import { useMemo } from 'react';
import { resolveParameters } from '~/util/model';
import { ExcerptText } from './ExcerptText';
import { Table } from './Table';
import { TSDoc } from './documentation/tsdoc/TSDoc';
const columnStyles = {
Name: 'font-mono whitespace-nowrap',
Type: 'font-mono whitespace-pre-wrap break-normal',
};
export function ParameterTable({ item }: { readonly item: ApiDocumentedItem & ApiParameterListMixin }) {
const params = resolveParameters(item);
const rows = useMemo(
() =>
params.map((param) => ({
Name: param.isRest ? `...${param.name}` : param.name,
Type: <ExcerptText excerpt={param.parameterTypeExcerpt} apiPackage={item.getAssociatedPackage()!} />,
Optional: param.isOptional ? 'Yes' : 'No',
Description: param.description ? <TSDoc item={item} tsdoc={param.description} /> : 'None',
})),
[item, params],
);
return (
<div className="overflow-x-auto">
<Table columnStyles={columnStyles} columns={['Name', 'Type', 'Optional', 'Description']} rows={rows} />
</div>
);
}

View File

@@ -1,49 +0,0 @@
import type {
ApiDeclaredItem,
ApiItemContainerMixin,
ApiProperty,
ApiPropertySignature,
} from '@discordjs/api-extractor-model';
import type { PropsWithChildren } from 'react';
import { Badges } from './Badges';
import { CodeHeading } from './CodeHeading';
import { ExcerptText } from './ExcerptText';
import { InheritanceText } from './InheritanceText';
import { TSDoc } from './documentation/tsdoc/TSDoc';
export function Property({
item,
children,
inheritedFrom,
}: PropsWithChildren<{
readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined;
readonly item: ApiProperty | ApiPropertySignature;
}>) {
const hasSummary = Boolean(item.tsdocComment?.summarySection);
return (
<div className="flex flex-col scroll-mt-30 gap-4" id={item.displayName}>
<div className="flex flex-col gap-2 md:-ml-9">
<Badges item={item} />
<CodeHeading
href={`#${item.displayName}`}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
>
{`${item.displayName}${item.isOptional ? '?' : ''}`}
<span>:</span>
{item.propertyTypeExcerpt.text ? (
<ExcerptText excerpt={item.propertyTypeExcerpt} apiPackage={item.getAssociatedPackage()!} />
) : null}
</CodeHeading>
</div>
{hasSummary || inheritedFrom ? (
<div className="mb-4 w-full flex flex-col gap-4">
{item.tsdocComment ? <TSDoc item={item} tsdoc={item.tsdocComment} /> : null}
{inheritedFrom ? <InheritanceText parent={inheritedFrom} /> : null}
{children}
</div>
) : null}
</div>
);
}

View File

@@ -1,37 +0,0 @@
import type {
ApiDeclaredItem,
ApiItem,
ApiItemContainerMixin,
ApiProperty,
ApiPropertySignature,
} from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { Fragment, useMemo } from 'react';
import { resolveMembers } from '~/util/members';
import { Property } from './Property';
export function isPropertyLike(item: ApiItem): item is ApiProperty | ApiPropertySignature {
return item.kind === ApiItemKind.Property || item.kind === ApiItemKind.PropertySignature;
}
export function PropertyList({ item }: { readonly item: ApiItemContainerMixin }) {
const members = resolveMembers(item, isPropertyLike);
const propertyItems = useMemo(
() =>
members.map((prop, idx) => {
return (
<Fragment key={`${prop.item.displayName}-${idx}`}>
<Property
inheritedFrom={prop.inherited as ApiDeclaredItem & ApiItemContainerMixin}
item={prop.item as ApiProperty}
/>
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</Fragment>
);
}),
[members],
);
return <div className="flex flex-col gap-4">{propertyItems}</div>;
}

View File

@@ -0,0 +1,98 @@
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExcerptNode } from './ExcerptNode';
import { InheritedFromNode } from './InheritedFromNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
export async function PropertyNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} />
Properties
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-8">
{node.map((property: any, idx: number) => {
return (
<Fragment key={`${property.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3
id={property.displayName}
className="group flex scroll-mt-8 flex-col gap-2 break-words font-mono font-semibold"
>
<Badges node={property} />
<span>
<Link
href={`#${property.displayName}`}
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block"
>
<LinkIcon aria-hidden size={16} />
</Link>
{property.displayName}
{property.isOptional ? '?' : ''} : <ExcerptNode node={property.typeExcerpt} version={version} />
</span>
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={property.sourceLine ? `${property.sourceURL}#L${property.sourceLine}` : property.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{property.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={property.summary.deprecatedBlock} version={version} />
) : null}
{property.summary?.summarySection.length ? (
<SummaryNode padding node={property.summary.summarySection} version={version} />
) : null}
{property.inheritedFrom ? (
<InheritedFromNode node={property.inheritedFrom} packageName={packageName} version={version} />
) : null}
{property.summary?.seeBlocks.length ? (
<SeeNode padding node={property.summary.seeBlocks} version={version} />
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -0,0 +1,17 @@
import { DocNode } from './DocNode';
export async function ReturnNode({
padding = false,
node,
version,
}: {
readonly node: any;
readonly padding?: boolean;
readonly version: string;
}) {
return (
<p className={`break-words ${padding ? 'pl-4' : ''}`}>
<span className="font-semibold">Returns:</span> <DocNode node={node} version={version} />
</p>
);
}

View File

@@ -1,8 +0,0 @@
'use client';
import type { ScrollbarProps } from 'react-custom-scrollbars-2';
import { Scrollbars as ReactScrollbars2 } from 'react-custom-scrollbars-2';
export function Scrollbars(props: ScrollbarProps) {
return <ReactScrollbars2 {...props} />;
}

View File

@@ -1,8 +0,0 @@
'use client';
import { Section as DJSSection, type SectionOptions } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
export function Section(options: PropsWithChildren<SectionOptions>) {
return <DJSSection {...options} />;
}

View File

@@ -0,0 +1,17 @@
import { DocNode } from './DocNode';
export async function SeeNode({
padding = false,
node,
version,
}: {
readonly node: any;
readonly padding?: boolean;
readonly version: string;
}) {
return (
<p className={`break-words ${padding ? 'pl-4' : ''}`}>
<span className="font-semibold">See also:</span> <DocNode node={node} version={version} />
</p>
);
}

View File

@@ -1,124 +0,0 @@
'use client';
import type { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import { useSelectedLayoutSegment } from 'next/navigation';
import { useMemo } from 'react';
import { useNav } from '~/contexts/nav';
import { ItemLink } from './ItemLink';
import { Section } from './Section';
export interface SidebarSectionItemData {
href: string;
kind: ApiItemKind;
name: string;
overloadIndex?: number | undefined;
}
interface GroupedMembers {
Classes: SidebarSectionItemData[];
Enums: SidebarSectionItemData[];
Functions: SidebarSectionItemData[];
Interfaces: SidebarSectionItemData[];
Types: SidebarSectionItemData[];
Variables: SidebarSectionItemData[];
}
function groupMembers(members: readonly SidebarSectionItemData[]): GroupedMembers {
const Classes: SidebarSectionItemData[] = [];
const Enums: SidebarSectionItemData[] = [];
const Interfaces: SidebarSectionItemData[] = [];
const Types: SidebarSectionItemData[] = [];
const Variables: SidebarSectionItemData[] = [];
const Functions: SidebarSectionItemData[] = [];
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: string) {
switch (item) {
case 'Classes':
return <VscSymbolClass size={20} />;
case 'Enums':
return <VscSymbolEnum size={20} />;
case 'Interfaces':
return <VscSymbolInterface size={20} />;
case 'Types':
case 'Variables':
return <VscSymbolVariable size={20} />;
default:
return <VscSymbolMethod size={20} />;
}
}
export function Sidebar({ members }: { readonly members: SidebarSectionItemData[] }) {
const segment = useSelectedLayoutSegment();
const { setOpened } = useNav();
const groupItems = useMemo(() => groupMembers(members), [members]);
return (
<div className="flex flex-col gap-4">
{(Object.keys(groupItems) as (keyof GroupedMembers)[])
.filter((group) => groupItems[group].length)
.map((group, idx) => (
<Section
buttonClassName="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-400 dark:hover:bg-dark-300 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-none focus:ring z-10"
icon={resolveIcon(group)}
key={`${group}-${idx}`}
title={group}
>
{groupItems[group].map((member, index) => (
<ItemLink
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l first:mt-1 p-[5px] pl-6 outline-none focus:rounded focus:border-0 focus:ring ${
decodeURIComponent(segment ?? '') === member.href
? 'bg-blurple text-white'
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'
}`}
itemURI={member.href}
key={`${member.name}-${index}`}
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>
</ItemLink>
))}
</Section>
))}
</div>
);
}

View File

@@ -1,10 +0,0 @@
import type { ApiPackage, Excerpt } from '@discordjs/api-extractor-model';
import { ExcerptText } from './ExcerptText';
export function SignatureText({ excerpt, apiPackage }: { readonly apiPackage: ApiPackage; readonly excerpt: Excerpt }) {
return (
<h4 className="break-all text-lg font-bold font-mono">
<ExcerptText excerpt={excerpt} apiPackage={apiPackage} />
</h4>
);
}

View File

@@ -0,0 +1,17 @@
import { DocNode } from './DocNode';
export async function SummaryNode({
padding = false,
node,
version,
}: {
readonly node: any;
readonly padding?: boolean;
readonly version: string;
}) {
return (
<p className={`break-words ${padding ? 'pl-4' : ''}`}>
<DocNode node={node} version={version} />
</p>
);
}

View File

@@ -1,14 +1,33 @@
import { Code } from 'bright';
import { getHighlighterCore } from 'shiki/core';
import getWasm from 'shiki/wasm';
const highlighter = await getHighlighterCore({
themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark-dimmed.mjs')],
langs: [import('shiki/langs/typescript.mjs'), import('shiki/langs/javascript.mjs')],
loadWasm: getWasm,
});
export async function SyntaxHighlighter({
lang,
code,
className = '',
}: {
readonly className?: string;
readonly code: string;
readonly lang: string;
}) {
const codeHTML = highlighter.codeToHtml(code.trim(), {
lang,
themes: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
});
export async function SyntaxHighlighter(props: typeof Code) {
return (
<>
<div data-theme="dark">
<Code codeClassName="font-mono" lang={props.lang ?? 'typescript'} {...props} theme="github-dark-dimmed" />
</div>
<div className="[&_pre]:border [&_pre]:border-gray-300 [&_pre]:rounded-md" data-theme="light">
<Code codeClassName="font-mono" lang={props.lang ?? 'typescript'} {...props} theme="min-light" />
</div>
{/* eslint-disable-next-line react/no-danger */}
<div className={className} dangerouslySetInnerHTML={{ __html: codeHTML }} />
</>
);
}

View File

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

View File

@@ -1,161 +0,0 @@
'use client';
import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { useMemo } from 'react';
export interface TableOfContentsSerializedMethod {
kind: 'Method' | 'MethodSignature';
name: string;
overloadIndex?: number;
}
export interface TableOfContentsSerializedProperty {
kind: 'Property' | 'PropertySignature';
name: string;
}
export interface TableOfContentsSerializedEvent {
kind: 'Event';
name: string;
}
export type TableOfContentsSerialized =
| TableOfContentsSerializedEvent
| TableOfContentsSerializedMethod
| TableOfContentsSerializedProperty;
export interface TableOfContentsItemProps {
readonly serializedMembers: TableOfContentsSerialized[];
}
export function TableOfContentsPropertyItem({ property }: { readonly property: TableOfContentsSerializedProperty }) {
return (
<a
className="ml-[10px] border-l border-light-800 p-[5px] pl-6.5 text-sm outline-none focus:border-0 dark:border-dark-100 focus:rounded active:bg-light-800 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-100 dark:hover:bg-dark-200"
href={`#${property.name}`}
key={`${property.name}-${property.kind}`}
title={property.name}
>
<span className="line-clamp-1">{property.name}</span>
</a>
);
}
export function TableOfContentsMethodItem({ method }: { readonly method: TableOfContentsSerializedMethod }) {
if (method.overloadIndex && method.overloadIndex > 1) {
return null;
}
const key = `${method.name}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`;
return (
<a
className="ml-[10px] flex flex-row place-items-center gap-2 border-l border-light-800 p-[5px] pl-6.5 text-sm outline-none focus:border-0 dark:border-dark-100 focus:rounded active:bg-light-800 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-100 dark:hover:bg-dark-200"
href={`#${key}`}
key={key}
title={method.name}
>
<span className="line-clamp-1">{method.name}</span>
{method.overloadIndex && method.overloadIndex > 1 ? (
<span className="text-xs">{method.overloadIndex}</span>
) : null}
</a>
);
}
export function TableOfContentsEventItem({ event }: { readonly event: TableOfContentsSerializedEvent }) {
return (
<a
className="ml-[10px] border-l border-light-800 p-[5px] pl-6.5 text-sm outline-none focus:border-0 dark:border-dark-100 focus:rounded active:bg-light-800 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-100 dark:hover:bg-dark-200"
href={`#${event.name}`}
key={`${event.name}-${event.kind}`}
title={event.name}
>
<span className="line-clamp-1">{event.name}</span>
</a>
);
}
export function TableOfContentItems({ serializedMembers }: TableOfContentsItemProps) {
const propertyItems = useMemo(
() =>
serializedMembers
.filter(
(member): member is TableOfContentsSerializedProperty =>
member.kind === 'Property' || member.kind === 'PropertySignature',
)
.map((prop, idx) => <TableOfContentsPropertyItem key={`${prop.name}-${prop.kind}-${idx}`} property={prop} />),
[serializedMembers],
);
const methodItems = useMemo(
() =>
serializedMembers
.filter(
(member): member is TableOfContentsSerializedMethod =>
member.kind === 'Method' || member.kind === 'MethodSignature',
)
.map((member, idx) => (
<TableOfContentsMethodItem
key={`${member.name}${member.overloadIndex ? `:${member.overloadIndex}` : ''}-${idx}`}
method={member}
/>
)),
[serializedMembers],
);
const eventItems = useMemo(
() =>
serializedMembers
.filter((member): member is TableOfContentsSerializedEvent => member.kind === 'Event')
.map((event, idx) => <TableOfContentsEventItem key={`${event.name}-${event.kind}-${idx}`} event={event} />),
[serializedMembers],
);
return (
<div className="flex flex-col break-all p-3 pb-8">
<div className="ml-2 mt-4 flex flex-row gap-2">
<VscListSelection size={25} />
<span className="font-semibold">Contents</span>
</div>
<div className="ml-2 mt-5.5 flex flex-col gap-2">
{eventItems.length ? (
<div className="flex flex-col">
<div className="flex flex-row place-items-center gap-4">
<VscSymbolEvent size={20} />
<div className="p-3 pl-0">
<span className="font-semibold">Events</span>
</div>
</div>
{eventItems}
</div>
) : null}
{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

@@ -1,20 +0,0 @@
'use client';
import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
import { Button } from 'ariakit/button';
import { useTheme } from 'next-themes';
export default function ThemeSwitcher() {
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
return (
<Button
aria-label="Toggle theme"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded rounded-full bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => toggleTheme()}
>
<VscColorMode size={24} />
</Button>
);
}

View File

@@ -1,39 +0,0 @@
import type { ApiTypeParameterListMixin } from '@discordjs/api-extractor-model';
import { useMemo } from 'react';
import { ExcerptText } from './ExcerptText';
import { Table } from './Table';
import { TSDoc } from './documentation/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({ item }: { readonly item: ApiTypeParameterListMixin }) {
const rows = useMemo(
() =>
item.typeParameters.map((typeParam) => ({
Name: typeParam.name,
Constraints: <ExcerptText excerpt={typeParam.constraintExcerpt} apiPackage={item.getAssociatedPackage()!} />,
Optional: typeParam.isOptional ? 'Yes' : 'No',
Default: <ExcerptText excerpt={typeParam.defaultTypeExcerpt} apiPackage={item.getAssociatedPackage()!} />,
Description: typeParam.tsdocTypeParamBlock ? (
<TSDoc item={item} tsdoc={typeParam.tsdocTypeParamBlock.content} />
) : (
'None'
),
})),
[item],
);
return (
<div className="overflow-x-auto">
<Table
columnStyles={rowElements}
columns={['Name', 'Constraints', 'Optional', 'Default', 'Description']}
rows={rows}
/>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import { LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DocNode } from './DocNode';
import { ExcerptNode } from './ExcerptNode';
export async function TypeParameterNode({
description = false,
node,
version,
}: {
readonly description?: boolean;
readonly node: any;
readonly version: string;
}) {
return (
<div className={`${description ? 'flex flex-col gap-8' : 'inline-block'}`}>
{node.map((typeParameter: any, idx: number) => {
return (
<Fragment key={`${typeParameter.name}-${idx}`}>
<div className={description ? '' : 'inline after:content-[",_"] last-of-type:after:content-none'}>
<h3 id={typeParameter.name} className="group inline scroll-mt-8 break-words font-mono font-semibold">
{description ? <Badges node={typeParameter} /> : null}
<span>
{description ? (
<Link
href={`#${typeParameter.name}`}
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block"
>
<LinkIcon aria-hidden size={16} />
</Link>
) : null}
{typeParameter.name}
{typeParameter.isOptional ? '?' : ''}
{typeParameter.constraintsExcerpt.length ? (
<>
{' extends '}
<ExcerptNode node={typeParameter.constraintsExcerpt} version={version} />
</>
) : null}
{typeParameter.defaultExcerpt.length ? (
<>
{' = '}
<ExcerptNode node={typeParameter.defaultExcerpt} version={version} />
</>
) : null}
</span>
</h3>
{description && typeParameter.description?.length ? (
<div className="pl-4">
<DocNode node={typeParameter.description} version={version} />
</div>
) : null}
</div>
</Fragment>
);
})}
{description ? (
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { ExcerptNode } from './ExcerptNode';
export async function UnionMember({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">Union Members</h2>
<span className="flex flex-col gap-4 break-words font-mono text-sm">
<ExcerptNode node={node} version={version} />
</span>
</div>
);
}

View File

@@ -1,69 +0,0 @@
'use client';
import { VscChevronDown } from '@react-icons/all-files/vsc/VscChevronDown';
import { VscVersions } from '@react-icons/all-files/vsc/VscVersions';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr';
const isDev = process.env.NEXT_PUBLIC_LOCAL_DEV === 'true' ?? process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview';
export default function VersionSelect({ versions }: { readonly versions: string[] }) {
const pathname = usePathname();
const packageName = pathname?.split('/').slice(3, 4)[0];
const branchName = pathname?.split('/').slice(4, 5)[0];
const { data } = useSWR<string[]>(packageName ? `/api/${packageName}/versions` : null, {
fallbackData: versions,
});
const versionMenu = useMenuState({
gutter: 8,
sameWidth: true,
fitViewport: true,
});
const versionMenuItems = useMemo(
() =>
data?.map((item, idx) => (
<Link href={`/docs/packages/${packageName}/${isDev ? 'main' : item}`} key={`${item}-${idx}`}>
<MenuItem
className="my-0.5 rounded bg-white p-3 text-sm outline-none active:bg-light-800 dark:bg-dark-600 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-500"
onClick={() => versionMenu.setOpen(false)}
state={versionMenu}
>
{item}
</MenuItem>
</Link>
)) ?? [],
[data, packageName, versionMenu],
);
return (
<>
<MenuButton
className="rounded bg-light-600 p-3 outline-none active:bg-light-800 dark:bg-dark-400 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-300"
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="z-20 flex flex-col border border-light-800 rounded bg-white p-1 outline-none dark:border-dark-100 dark:bg-dark-600 focus:ring focus:ring-width-2 focus:ring-blurple"
state={versionMenu}
>
{versionMenuItems}
</Menu>
</>
);
}

View File

@@ -1,8 +0,0 @@
import type { PropsWithChildren } from 'react';
/**
* Layout parent of documentation pages.
*/
export function Documentation({ children }: PropsWithChildren) {
return <div className="w-full flex flex-col gap-4">{children}</div>;
}

View File

@@ -1,51 +0,0 @@
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import type { PropsWithChildren } from 'react';
import { SourceLink } from './SourceLink';
function generateIcon(kind: ApiItemKind) {
switch (kind) {
case ApiItemKind.Class:
return <VscSymbolClass />;
case ApiItemKind.Function:
case ApiItemKind.Method:
return <VscSymbolMethod />;
case ApiItemKind.Enum:
return <VscSymbolEnum />;
case ApiItemKind.Interface:
return <VscSymbolInterface />;
case ApiItemKind.TypeAlias:
case ApiItemKind.Variable:
return <VscSymbolVariable />;
default:
return <VscSymbolMethod />;
}
}
export function Header({
kind,
name,
sourceURL,
sourceLine,
}: PropsWithChildren<{
readonly kind: ApiItemKind;
readonly name: string;
readonly sourceLine?: number | undefined;
readonly sourceURL?: string | undefined;
}>) {
return (
<div className="flex flex-col">
<h2 className="flex flex-row place-items-center justify-between gap-2 break-all text-2xl font-bold">
<span className="row flex flex place-items-center gap-2">
<span>{generateIcon(kind)}</span>
{name}
</span>
{sourceURL ? <SourceLink sourceLine={sourceLine} sourceURL={sourceURL} /> : null}
</h2>
</div>
);
}

View File

@@ -1,57 +0,0 @@
import type { ApiClass, ApiInterface, Excerpt } from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { ExcerptText } from '../ExcerptText';
export function HierarchyText({
item,
type,
}: {
readonly item: ApiClass | ApiInterface;
readonly type: 'Extends' | 'Implements';
}) {
if (
(item.kind === ApiItemKind.Class &&
(item as ApiClass).extendsType === undefined &&
(item as ApiClass).implementsTypes.length === 0) ||
(item.kind === ApiItemKind.Interface && !(item as ApiInterface).extendsTypes)
) {
return null;
}
let excerpts: Excerpt[];
if (item.kind === ApiItemKind.Class) {
if (type === 'Implements') {
if ((item as ApiClass).implementsTypes.length === 0) {
return null;
}
excerpts = (item as ApiClass).implementsTypes.map((typeExcerpt) => typeExcerpt.excerpt);
} else {
if (!(item as ApiClass).extendsType) {
return null;
}
excerpts = [(item as ApiClass).extendsType!.excerpt];
}
} else {
if ((item as ApiInterface).extendsTypes.length === 0) {
return null;
}
excerpts = (item as ApiInterface).extendsTypes.map((typeExcerpt) => typeExcerpt.excerpt);
}
return (
<div className="flex flex-col gap-4">
{excerpts.map((excerpt, idx) => (
<div className="flex flex-row place-items-center gap-4" key={`${type}-${idx}`}>
<h3 className="text-xl font-bold">{type}</h3>
<span className="break-all font-mono space-y-2">
<ExcerptText excerpt={excerpt} apiPackage={item.getAssociatedPackage()!} />
</span>
</div>
))}
</div>
);
}

View File

@@ -1,15 +0,0 @@
import type { ApiDeclaredItem, ApiItemContainerMixin } from '@discordjs/api-extractor-model';
import { EventsSection } from './section/EventsSection';
import { MethodsSection } from './section/MethodsSection';
import { PropertiesSection } from './section/PropertiesSection';
import { hasEvents, hasProperties, hasMethods } from './util';
export function Members({ item }: { readonly item: ApiDeclaredItem & ApiItemContainerMixin }) {
return (
<>
{hasEvents(item) ? <EventsSection item={item} /> : null}
{hasProperties(item) ? <PropertiesSection item={item} /> : null}
{hasMethods(item) ? <MethodsSection item={item} /> : null}
</>
);
}

View File

@@ -1,24 +0,0 @@
import type { ApiDeclaredItem } from '@discordjs/api-extractor-model';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { Header } from './Header';
import { SummarySection } from './section/SummarySection';
export interface ObjectHeaderProps {
readonly item: ApiDeclaredItem;
}
export function ObjectHeader({ item }: ObjectHeaderProps) {
return (
<>
<Header
kind={item.kind}
name={item.displayName}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
/>
{/* @ts-expect-error async component */}
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
</>
);
}

View File

@@ -1,22 +0,0 @@
import { VscFileCode } from '@react-icons/all-files/vsc/VscFileCode';
export function SourceLink({
className,
sourceURL,
sourceLine,
}: {
readonly className?: string | undefined;
readonly sourceLine?: number | undefined;
readonly sourceURL?: string | undefined;
}) {
return (
<a
className={` text-blurple ${className}`}
href={sourceLine ? `${sourceURL}#L${sourceLine}` : sourceURL}
rel="external noopener noreferrer"
target="_blank"
>
<VscFileCode />
</a>
);
}

View File

@@ -1,22 +0,0 @@
import type { ApiConstructor } from '@discordjs/api-extractor-model';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { CodeHeading } from '~/components/CodeHeading';
import { ParameterTable } from '../../ParameterTable';
import { TSDoc } from '../tsdoc/TSDoc';
import { parametersString } from '../util';
import { DocumentationSection } from './DocumentationSection';
export function ConstructorSection({ item }: { readonly item: ApiConstructor }) {
return (
<DocumentationSection icon={<VscSymbolMethod size={20} />} padded title="Constructor">
<div className="flex flex-col gap-2">
<CodeHeading
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
>{`constructor(${parametersString(item)})`}</CodeHeading>
{item.tsdocComment ? <TSDoc item={item} tsdoc={item.tsdocComment} /> : null}
<ParameterTable item={item} />
</div>
</DocumentationSection>
);
}

View File

@@ -1,14 +0,0 @@
import type { SectionOptions } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
import { Section } from '../../Section';
export function DocumentationSection(opts: PropsWithChildren<SectionOptions & { separator?: boolean }>) {
const { children, separator, ...props } = opts;
return (
<Section {...props}>
{children}
{separator ? <div className="mt-6 border-t-2 border-light-900 dark:border-dark-100" /> : null}
</Section>
);
}

View File

@@ -1,42 +0,0 @@
import {
ApiItemKind,
type ApiEvent,
type ApiItem,
type ApiItemContainerMixin,
type ApiDeclaredItem,
} from '@discordjs/api-extractor-model';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { Fragment, useMemo } from 'react';
import { Event } from '~/components/model/Event';
import { resolveMembers } from '~/util/members';
import { DocumentationSection } from './DocumentationSection';
function isEventLike(item: ApiItem): item is ApiEvent {
return item.kind === ApiItemKind.Event;
}
export function EventsSection({ item }: { readonly item: ApiItemContainerMixin }) {
const members = resolveMembers(item, isEventLike);
const eventItems = useMemo(
() =>
members.map((event, idx) => {
return (
<Fragment key={`${event.item.displayName}-${idx}`}>
<Event
inheritedFrom={event.inherited as ApiDeclaredItem & ApiItemContainerMixin}
item={event.item as ApiEvent}
/>
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</Fragment>
);
}),
[members],
);
return (
<DocumentationSection icon={<VscSymbolEvent size={20} />} padded title="Events">
<div className="flex flex-col gap-4">{eventItems}</div>
</DocumentationSection>
);
}

Some files were not shown because too many files have changed in this diff Show More