diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..f6640d765 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Go to https://app.gitbook.com/account/developer and create a new token +# Copy the token and paste it in the .env file +# GITBOOK_TOKEN=your_token_here + +# Other options: +# GITBOOK_API_URL=https://api.gitbook.com diff --git a/.eslintrc.json b/.eslintrc.json index bffb357a7..1c2aa65d7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": "next/core-web-vitals" } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..a680367ef --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +.next diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..581887eed --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "singleQuote": true, + "tabWidth": 4 +} diff --git a/README.md b/README.md index c4033664f..e69de29bb 100644 --- a/README.md +++ b/README.md @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/bun.lockb b/bun.lockb index b4730dd56..bad6bd4d1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/next.config.js b/next.config.js index 767719fc4..658404ac6 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,4 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = {}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/package.json b/package.json index 258fe4591..e1eeb10eb 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,33 @@ { - "name": "gitbook", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "react": "^18", - "react-dom": "^18", - "next": "13.5.6" - }, - "devDependencies": { - "typescript": "^5", - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10", - "postcss": "^8", - "tailwindcss": "^3", - "eslint": "^8", - "eslint-config-next": "13.5.6" - } + "name": "gitbook", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "prettier ./ --ignore-unknown --write", + "format:check": "prettier ./ --ignore-unknown --list-different" + }, + "dependencies": { + "@gitbook/api": "^0.11.0", + "clsx": "^2.0.0", + "next": "13.5.6", + "react": "^18", + "react-dom": "^18", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10", + "eslint": "^8", + "eslint-config-next": "13.5.6", + "postcss": "^8", + "prettier": "^3.0.3", + "tailwindcss": "^3", + "typescript": "^5" + } } diff --git a/postcss.config.js b/postcss.config.js index 33ad091d2..67cdf1a55 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28c5..000000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f842227..000000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/[[...pathname]]/page.tsx b/src/app/[[...pathname]]/page.tsx new file mode 100644 index 000000000..a48368ef7 --- /dev/null +++ b/src/app/[[...pathname]]/page.tsx @@ -0,0 +1,30 @@ +import { headers } from 'next/headers'; + +import { api } from '@/lib/api'; +import { SpaceContent } from '@/components/SpaceContent'; + +export default async function Page(props: { params: { pathname?: string[] } }) { + const { + params: { pathname }, + } = props; + + const headersList = headers(); + const spaceId = headersList.get('x-gitbook-space'); + + if (!spaceId) { + throw new Error('Missing space id'); + } + + const [{ data: space }, { data: revision }] = await Promise.all([ + api().spaces.getSpaceById(spaceId), + api().spaces.getCurrentRevision(spaceId), + ]); + + return ( + + ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fea4..000000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css index fd81e8858..68cb03319 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,26 +2,5 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 40e027fbe..bc255000d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,22 +1,12 @@ -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -import './globals.css' +import { Inter } from 'next/font/google'; +import './globals.css'; -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ['latin'] }); -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index e38c626e1..000000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import Image from 'next/image' - -export default function Home() { - return ( -
-
-

- Get started by editing  - src/app/page.tsx -

-
- - By{' '} - Vercel Logo - -
-
- -
- Next.js Logo -
- -
- -

- Docs{' '} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{' '} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{' '} - - -> - -

-

- Explore the Next.js 13 playground. -

-
- - -

- Deploy{' '} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
-
- ) -} diff --git a/src/components/DocumentView/Block.tsx b/src/components/DocumentView/Block.tsx new file mode 100644 index 000000000..4667f0afd --- /dev/null +++ b/src/components/DocumentView/Block.tsx @@ -0,0 +1,32 @@ +import { ClassValue } from 'clsx'; +import { Paragraph } from './Paragraph'; +import { Heading } from './Heading'; +import { ListOrdered } from './ListOrdered'; +import { ListUnordered } from './ListUnordered'; +import { ListItem } from './ListItem'; + +export interface BlockProps { + block: T; + style?: ClassValue; +} + +export function Block(props: BlockProps) { + const { block } = props; + + switch (block.type) { + case 'paragraph': + return ; + case 'heading-1': + case 'heading-2': + case 'heading-3': + return ; + case 'list-ordered': + return ; + case 'list-unordered': + return ; + case 'list-item': + return ; + default: + return
Unsupported block {block.type}
; + } +} diff --git a/src/components/DocumentView/Blocks.tsx b/src/components/DocumentView/Blocks.tsx new file mode 100644 index 000000000..765533631 --- /dev/null +++ b/src/components/DocumentView/Blocks.tsx @@ -0,0 +1,41 @@ +import clsx from 'clsx'; +import { ClassValue } from 'clsx'; +import { Block } from './Block'; + +export function Blocks(props: { + /** Blocks to render */ + nodes: T[]; + + /** HTML tag to use */ + tag?: string; + + /** Style passed to the wrapper */ + style?: ClassValue; + + /** Style passed to all blocks */ + blockStyle?: ClassValue; + + /** Style passed to the first block */ + firstBlockStyle?: ClassValue; + + /** Style passed to the last block */ + lastBlockStyle?: ClassValue; +}) { + const { nodes, tag: Tag = 'div', style, blockStyle, firstBlockStyle, lastBlockStyle } = props; + + return ( + + {nodes.map((node, index) => ( + + ))} + + ); +} diff --git a/src/components/DocumentView/DocumentView.tsx b/src/components/DocumentView/DocumentView.tsx new file mode 100644 index 000000000..b29481835 --- /dev/null +++ b/src/components/DocumentView/DocumentView.tsx @@ -0,0 +1,11 @@ +import clsx from 'clsx'; +import { Blocks } from './Blocks'; + +/** + * Render an entire document. + */ +export function DocumentView(props: { document: any }) { + const { document } = props; + + return ; +} diff --git a/src/components/DocumentView/Heading.tsx b/src/components/DocumentView/Heading.tsx new file mode 100644 index 000000000..f5cf0b589 --- /dev/null +++ b/src/components/DocumentView/Heading.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; +import { BlockProps } from './Block'; +import { Inlines } from './Inlines'; + +export function Heading(props: BlockProps) { + const { block, style } = props; + + const headingStyle = STYLES[block.type]; + + return ( + + + + ); +} + +const STYLES = { + 'heading-1': { + tag: 'h2', + className: ['text-3xl', 'font-semibold'], + }, + 'heading-2': { + tag: 'h3', + className: ['text-2xl', 'font-semibold'], + }, + 'heading-3': { + tag: 'h4', + className: ['text-3xl', 'font-semibold'], + }, +}; diff --git a/src/components/DocumentView/Inline.tsx b/src/components/DocumentView/Inline.tsx new file mode 100644 index 000000000..13d9221ab --- /dev/null +++ b/src/components/DocumentView/Inline.tsx @@ -0,0 +1,14 @@ +import { ClassValue } from 'clsx'; + +export interface InlineProps { + inline: T; +} + +export function Inline(props: InlineProps) { + const { inline } = props; + + switch (inline.type) { + default: + return Unsupported inline {inline.type}; + } +} diff --git a/src/components/DocumentView/Inlines.tsx b/src/components/DocumentView/Inlines.tsx new file mode 100644 index 000000000..4ced5b699 --- /dev/null +++ b/src/components/DocumentView/Inlines.tsx @@ -0,0 +1,19 @@ +import clsx from 'clsx'; +import { Text } from './Text'; +import { Inline } from './Inline'; + +export function Inlines(props: { nodes: T[] }) { + const { nodes } = props; + + return ( + <> + {nodes.map((node, index) => { + if (node.object === 'text') { + return ; + } + + return ; + })} + + ); +} diff --git a/src/components/DocumentView/ListItem.tsx b/src/components/DocumentView/ListItem.tsx new file mode 100644 index 000000000..60ae4eb05 --- /dev/null +++ b/src/components/DocumentView/ListItem.tsx @@ -0,0 +1,13 @@ +import clsx from 'clsx'; +import { BlockProps } from './Block'; +import { Blocks } from './Blocks'; + +export function ListItem(props: BlockProps) { + const { block } = props; + + return ( +
  • + +
  • + ); +} diff --git a/src/components/DocumentView/ListOrdered.tsx b/src/components/DocumentView/ListOrdered.tsx new file mode 100644 index 000000000..ad251441b --- /dev/null +++ b/src/components/DocumentView/ListOrdered.tsx @@ -0,0 +1,8 @@ +import { BlockProps } from './Block'; +import { Blocks } from './Blocks'; + +export function ListOrdered(props: BlockProps) { + const { block, style } = props; + + return ; +} diff --git a/src/components/DocumentView/ListUnordered.tsx b/src/components/DocumentView/ListUnordered.tsx new file mode 100644 index 000000000..c487698d1 --- /dev/null +++ b/src/components/DocumentView/ListUnordered.tsx @@ -0,0 +1,8 @@ +import { BlockProps } from './Block'; +import { Blocks } from './Blocks'; + +export function ListUnordered(props: BlockProps) { + const { block, style } = props; + + return ; +} diff --git a/src/components/DocumentView/Paragraph.tsx b/src/components/DocumentView/Paragraph.tsx new file mode 100644 index 000000000..9ffcfc163 --- /dev/null +++ b/src/components/DocumentView/Paragraph.tsx @@ -0,0 +1,13 @@ +import clsx from 'clsx'; +import { BlockProps } from './Block'; +import { Inlines } from './Inlines'; + +export function Paragraph(props: BlockProps) { + const { block, style } = props; + + return ( +

    + +

    + ); +} diff --git a/src/components/DocumentView/Text.tsx b/src/components/DocumentView/Text.tsx new file mode 100644 index 000000000..552fde952 --- /dev/null +++ b/src/components/DocumentView/Text.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export function Text(props: { text: any }) { + const { text } = props; + + return ( + <> + {text.leaves.map((leaf, index) => { + // TODO: marks + return {leaf.text}; + })} + + ); +} diff --git a/src/components/DocumentView/index.ts b/src/components/DocumentView/index.ts new file mode 100644 index 000000000..2b742d2d5 --- /dev/null +++ b/src/components/DocumentView/index.ts @@ -0,0 +1 @@ +export * from './DocumentView'; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 000000000..b345e9c93 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,27 @@ +import { pageHref } from '@/lib/links'; +import { Space } from '@gitbook/api'; + +import clsx from 'clsx'; +import Link from 'next/link'; + +/** + * Render the header for the space. + */ +export function Header(props: { space: Space }) { + const { space } = props; + + return ( +
    +
    + +

    {space.title}

    + +
    +
    + ); +} diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 000000000..266dec8a1 --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/components/PageBody/PageBody.tsx b/src/components/PageBody/PageBody.tsx new file mode 100644 index 000000000..ae65a897f --- /dev/null +++ b/src/components/PageBody/PageBody.tsx @@ -0,0 +1,14 @@ +import { Revision, RevisionPageDocument, Space } from '@gitbook/api'; +import { PageHeader } from './PageHeader'; +import { PageDocument } from './PageDocument'; + +export function PageBody(props: { space: Space; revision: Revision; page: RevisionPageDocument }) { + const { space, revision, page } = props; + + return ( +
    + + +
    + ); +} diff --git a/src/components/PageBody/PageDocument.tsx b/src/components/PageBody/PageDocument.tsx new file mode 100644 index 000000000..ce681873c --- /dev/null +++ b/src/components/PageBody/PageDocument.tsx @@ -0,0 +1,20 @@ +import { api } from '@/lib/api'; +import { Revision, RevisionPageDocument, Space } from '@gitbook/api'; +import { DocumentView } from '../DocumentView'; + +/** + * Fetch the document for the page and render it. + */ +export async function PageDocument(props: { + space: Space; + revision: Revision; + page: RevisionPageDocument; +}) { + const { space, revision, page } = props; + + const { + data: { document }, + } = await api().spaces.getPageInRevisionById(space.id, revision.id, page.id); + + return ; +} diff --git a/src/components/PageBody/PageHeader.tsx b/src/components/PageBody/PageHeader.tsx new file mode 100644 index 000000000..6ede8ca03 --- /dev/null +++ b/src/components/PageBody/PageHeader.tsx @@ -0,0 +1,15 @@ +import { RevisionPageDocument } from '@gitbook/api'; +import clsx from 'clsx'; + +export function PageHeader(props: { page: RevisionPageDocument }) { + const { page } = props; + + return ( +
    +

    {page.title}

    + {page.description ? ( +

    {page.description}

    + ) : null} +
    + ); +} diff --git a/src/components/PageBody/index.ts b/src/components/PageBody/index.ts new file mode 100644 index 000000000..9cc5a3f24 --- /dev/null +++ b/src/components/PageBody/index.ts @@ -0,0 +1 @@ +export * from './PageBody'; diff --git a/src/components/SpaceContent/SpaceContent.tsx b/src/components/SpaceContent/SpaceContent.tsx new file mode 100644 index 000000000..4e420b9a3 --- /dev/null +++ b/src/components/SpaceContent/SpaceContent.tsx @@ -0,0 +1,95 @@ +import { Revision, RevisionPage, RevisionPageDocument, Space } from '@gitbook/api'; + +import { TableOfContents } from '@/components/TableOfContents'; +import clsx from 'clsx'; +import { Header } from '@/components/Header'; +import { PageBody } from '@/components/PageBody'; +import { notFound, redirect } from 'next/navigation'; +import { pageHref } from '@/lib/links'; + +/** + * Render the entire content of the space (header, table of contents, footer, and page content). + */ +export function SpaceContent(props: { space: Space; revision: Revision; pagePath: string }) { + const { space, revision, pagePath } = props; + const activePage = resolvePagePath(revision, pagePath); + + return ( +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + ); +} + +function resolvePagePath(revision: Revision, pagePath: string): RevisionPageDocument { + const resolveFirstDocument = (pages: RevisionPage[]): RevisionPageDocument | undefined => { + for (const page of pages) { + if (page.type === 'link') { + continue; + } + + return resolvePage(page); + } + + return; + }; + + const resolvePage = (page: RevisionPage): RevisionPageDocument => { + if (page.type === 'group') { + const firstDocument = resolveFirstDocument(page.pages); + if (firstDocument) { + redirect(pageHref(firstDocument.path)); + } + + notFound(); + } else if (page.type === 'link') { + notFound(); + } + + return page; + }; + + const iteratePages = (pages: RevisionPage[]): RevisionPageDocument | undefined => { + for (const page of pages) { + if (page.type === 'link') { + continue; + } + + if (page.path !== pagePath) { + // TODO: can be optimized to count the number of slashes and skip the entire subtree + const result = iteratePages(page.pages); + if (result) { + return result; + } + + continue; + } + + return resolvePage(page); + } + }; + + if (!pagePath) { + const firstPage = resolveFirstDocument(revision.pages); + if (!firstPage) { + notFound(); + } + + return firstPage; + } + + const result = iteratePages(revision.pages); + if (!result) { + notFound(); + } + + return result; +} diff --git a/src/components/SpaceContent/index.ts b/src/components/SpaceContent/index.ts new file mode 100644 index 000000000..6436c15b8 --- /dev/null +++ b/src/components/SpaceContent/index.ts @@ -0,0 +1 @@ +export * from './SpaceContent'; diff --git a/src/components/TableOfContents/PageDocumentItem.tsx b/src/components/TableOfContents/PageDocumentItem.tsx new file mode 100644 index 000000000..721d60736 --- /dev/null +++ b/src/components/TableOfContents/PageDocumentItem.tsx @@ -0,0 +1,36 @@ +import { RevisionPageDocument } from '@gitbook/api'; +import { clsx } from 'clsx'; +import Link from 'next/link'; +import { PagesList } from './PagesList'; +import { pageHref } from '@/lib/links'; + +export function PageDocumentItem(props: { + page: RevisionPageDocument; + activePage: RevisionPageDocument; +}) { + const { page, activePage } = props; + + return ( +
  • + + {page.title} + + {page.pages && page.pages.length ? ( + + ) : null} +
  • + ); +} diff --git a/src/components/TableOfContents/PageGroupItem.tsx b/src/components/TableOfContents/PageGroupItem.tsx new file mode 100644 index 000000000..32baa9a8d --- /dev/null +++ b/src/components/TableOfContents/PageGroupItem.tsx @@ -0,0 +1,21 @@ +import { RevisionPageDocument, RevisionPageGroup } from '@gitbook/api'; +import { clsx } from 'clsx'; +import { PagesList } from './PagesList'; + +export function PageGroupItem(props: { + page: RevisionPageGroup; + activePage: RevisionPageDocument; +}) { + const { page, activePage } = props; + + return ( +
  • +
    + {page.title} +
    + {page.pages && page.pages.length ? ( + + ) : null} +
  • + ); +} diff --git a/src/components/TableOfContents/PagesList.tsx b/src/components/TableOfContents/PagesList.tsx new file mode 100644 index 000000000..4cf7db72c --- /dev/null +++ b/src/components/TableOfContents/PagesList.tsx @@ -0,0 +1,24 @@ +import { RevisionPage, RevisionPageDocument } from '@gitbook/api'; +import { ClassValue, clsx } from 'clsx'; +import { PageDocumentItem } from './PageDocumentItem'; +import { PageGroupItem } from './PageGroupItem'; + +export function PagesList(props: { + pages: RevisionPage[]; + activePage: RevisionPageDocument; + style?: ClassValue; +}) { + const { pages, activePage, style } = props; + + return ( +
      + {pages.map((page) => { + if (page.type === 'group') { + return ; + } + + return ; + })} +
    + ); +} diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx new file mode 100644 index 000000000..fbf0516fc --- /dev/null +++ b/src/components/TableOfContents/TableOfContents.tsx @@ -0,0 +1,22 @@ +import { Revision, RevisionPageDocument } from '@gitbook/api'; +import { clsx } from 'clsx'; +import { PagesList } from './PagesList'; +import { Trademark } from './Trademark'; + +export function TableOfContents(props: { revision: Revision; activePage: RevisionPageDocument }) { + const { revision, activePage } = props; + + return ( + + ); +} diff --git a/src/components/TableOfContents/Trademark.tsx b/src/components/TableOfContents/Trademark.tsx new file mode 100644 index 000000000..0a1a89587 --- /dev/null +++ b/src/components/TableOfContents/Trademark.tsx @@ -0,0 +1,28 @@ +import clsx from 'clsx'; + +/** + * Link to the GitBook platform. + */ +export function Trademark() { + return ( + + ); +} diff --git a/src/components/TableOfContents/index.ts b/src/components/TableOfContents/index.ts new file mode 100644 index 000000000..715a3a239 --- /dev/null +++ b/src/components/TableOfContents/index.ts @@ -0,0 +1 @@ +export * from './TableOfContents'; diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 000000000..56c198887 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,19 @@ +import 'server-only'; + +import { headers } from 'next/headers'; +import { GitBookAPI } from '@gitbook/api'; + +/** + * Create an API client for the current request. + */ +export function api(): GitBookAPI { + const headersList = headers(); + const apiToken = headersList.get('x-gitbook-token'); + + const gitbook = new GitBookAPI({ + authToken: apiToken || process.env.GITBOOK_TOKEN, + endpoint: process.env.GITBOOK_API_URL, + }); + + return gitbook; +} diff --git a/src/lib/links.ts b/src/lib/links.ts new file mode 100644 index 000000000..889627d44 --- /dev/null +++ b/src/lib/links.ts @@ -0,0 +1,13 @@ +import 'server-only'; + +import { headers } from 'next/headers'; + +/** + * Create a link to a page path in the current space. + */ +export function pageHref(pagePath: string): string { + const headersList = headers(); + const basePath = headersList.get('x-gitbook-basepath') ?? ''; + + return `${basePath}/${pagePath.startsWith('/') ? pagePath.slice(1) : pagePath}`; +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 000000000..55fe554a3 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; + +/** + * Rewrite the request to extract the spaceId from the URL + * and pass it as a header. + */ +export function middleware(request: NextRequest) { + const url = new URL(request.url); + const [space, ...pathRest] = url.pathname.slice(1).split('/'); + + const headers = new Headers(request.headers); + headers.set('x-gitbook-space', space); + headers.set('x-gitbook-basepath', `/${space}`); + + url.pathname = `/${pathRest.join('/')}`; + + return NextResponse.rewrite(url, { + headers, + }); +} + +export const config = { + matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)', +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 1af3b8f01..e23395f3d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,20 +1,19 @@ -import type { Config } from 'tailwindcss' +import type { Config } from 'tailwindcss'; +import colors from 'tailwindcss/colors'; const config: Config = { - content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: { - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + primary: colors.blue, + }, + }, }, - }, - plugins: [], -} -export default config + plugins: [], +}; +export default config; diff --git a/tsconfig.json b/tsconfig.json index e59724b28..1445a451f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] }