From b17527cd6c64f5ca6da346c84d10e0c7aa49f6bd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Mon, 11 May 2020 23:56:07 -0400 Subject: [PATCH 1/3] feat: implement table of contents --- gatsby-config.js | 6 ++++ package-lock.json | 24 +++++++++++++++ package.json | 1 + src/components/PageContent/index.tsx | 13 ++++++++- src/components/PageContent/styles.tsx | 8 ++++- src/components/Toc/index.tsx | 42 +++++++++++++++++++++++++++ src/components/Toc/styles.tsx | 30 +++++++++++++++++++ src/pages/about.tsx | 15 ++++++++-- src/pages/rules.tsx | 12 ++++++-- src/templates/languagePost.tsx | 29 ++++++++++++++---- src/types/index.d.ts | 6 ++++ src/utils/index.ts | 22 ++++++++++++-- 12 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 src/components/Toc/index.tsx create mode 100644 src/components/Toc/styles.tsx diff --git a/gatsby-config.js b/gatsby-config.js index d2a3b1e..c97803c 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -71,6 +71,12 @@ module.exports = { }, }, `gatsby-transformer-sharp`, + { + resolve: "gatsby-plugin-anchor-links", + options: { + offset: -100, + }, + }, `gatsby-plugin-react-svg`, `gatsby-plugin-remove-trailing-slashes`, `gatsby-plugin-sharp`, diff --git a/package-lock.json b/package-lock.json index 01df4a7..00f1869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9627,6 +9627,14 @@ "micromatch": "^3.1.10" } }, + "gatsby-plugin-anchor-links": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/gatsby-plugin-anchor-links/-/gatsby-plugin-anchor-links-1.1.1.tgz", + "integrity": "sha512-mgSHUAEa7RRWD/lcmJVUWD9mIT14EIJvMPcxJ1W2Ev+rNuqBBpk1j4vDWy1uxas+qusi0vrtZ4HdU7mse12qDw==", + "requires": { + "scroll-to-element": "^2.0.3" + } + }, "gatsby-plugin-layout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/gatsby-plugin-layout/-/gatsby-plugin-layout-1.3.1.tgz", @@ -16734,6 +16742,14 @@ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, "ramda": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", @@ -18233,6 +18249,14 @@ "invariant": "^2.2.4" } }, + "scroll-to-element": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/scroll-to-element/-/scroll-to-element-2.0.3.tgz", + "integrity": "sha512-5herPcm9jMfQgRwu94lH5mei+2YhipR4RQ2nAvnBxJb2tG+P7O0ctOKAaAZBXbBejnn+MImh3wrAUA5EcLnjEQ==", + "requires": { + "raf": "^3.4.0" + } + }, "scuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", diff --git a/package.json b/package.json index fa4d5ad..99807ca 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@reach/router": "^1.3.3", "classnames": "^2.2.6", "gatsby": "^2.21.21", + "gatsby-plugin-anchor-links": "^1.1.1", "gatsby-plugin-layout": "^1.3.1", "gatsby-plugin-manifest": "^2.4.2", "gatsby-plugin-prefetch-google-fonts": "^1.4.3", diff --git a/src/components/PageContent/index.tsx b/src/components/PageContent/index.tsx index a2dd440..4cc4b85 100644 --- a/src/components/PageContent/index.tsx +++ b/src/components/PageContent/index.tsx @@ -1,11 +1,14 @@ import React, { FC, Fragment } from "react" +import { ITocItem } from "../../types" import { humanize } from "../../utils" -import * as SC from "./styles" import { Container } from "../Container" +import { Toc } from "../Toc" +import * as SC from "./styles" interface IPageContentProps { content: JSX.Element + toc?: ITocItem[] recommendedReading?: string[] externalResources?: string[] } @@ -42,6 +45,7 @@ function ExtraLink({ export const PageContent: FC = ({ content, + toc = [], recommendedReading, externalResources, }) => { @@ -52,6 +56,13 @@ export const PageContent: FC = ({ + {toc.length > 0 && ( + Table of Contents} + items={toc} + /> + )} + {recommendedReading && ( Recommended reading diff --git a/src/components/PageContent/styles.tsx b/src/components/PageContent/styles.tsx index 0d22aa0..31922a7 100644 --- a/src/components/PageContent/styles.tsx +++ b/src/components/PageContent/styles.tsx @@ -21,6 +21,8 @@ export const Sidebar = styled.div` color: ${props => (props.theme.name === "dark" ? "#f9f9f9" : "#172129")}; margin: 64px 0; padding: 0 32px; + position: sticky; + top: 32px; &:empty { display: none; @@ -50,7 +52,11 @@ export const SidebarHeader = styled.div` ` const ExtraLinks = styled.div` - margin-top: 16px; + margin-top: 32px; + + &:first-child { + margin-top: 0; + } ` export const RecommendedReading = styled(ExtraLinks)`` diff --git a/src/components/Toc/index.tsx b/src/components/Toc/index.tsx new file mode 100644 index 0000000..48c1016 --- /dev/null +++ b/src/components/Toc/index.tsx @@ -0,0 +1,42 @@ +import React, { FC } from "react" +import { ITocItem } from "../../types" +import * as SC from "./styles" + +interface ITocProps { + header: React.ReactNode + items: ITocItem[] +} + +interface ITitle { + prefix?: string + title: string +} + +function extractTitle(title: string): ITitle { + const maybePrefix = title.match(/^(\w+\.)/) + const rest = maybePrefix ? title.replace(maybePrefix[0], "") : title + + return { + prefix: maybePrefix ? maybePrefix[0] : undefined, + title: rest, + } +} + +export const Toc: FC = ({ header, items }) => { + const { pathname } = window.location + return ( + + {header} + {items.map(item => { + const { prefix, title } = extractTitle(item.title) + + return ( + + {prefix} + {title} + + ) + })} + + ) +} diff --git a/src/components/Toc/styles.tsx b/src/components/Toc/styles.tsx new file mode 100644 index 0000000..d2cb591 --- /dev/null +++ b/src/components/Toc/styles.tsx @@ -0,0 +1,30 @@ +import { AnchorLink } from "gatsby-plugin-anchor-links" +import styled from "styled-components" + +export const TocWrapper = styled.div` + display: flex; + flex-direction: column; +` + +export const TocItem = styled.div` + display: inline-block; + + &.depth-3 { + margin-left: 8px; + } + + &.depth-2 + &.depth-2, + &.depth-3 + &.depth-2 { + margin-top: 8px; + } +` + +export const TocLink = styled(AnchorLink)` + color: ${props => props.theme.main.foreground}; + text-decoration: none; + + &:hover, + &:focus { + text-decoration: underline; + } +` diff --git a/src/pages/about.tsx b/src/pages/about.tsx index 15be243..d28996d 100644 --- a/src/pages/about.tsx +++ b/src/pages/about.tsx @@ -1,21 +1,28 @@ import { graphql } from "gatsby" import React, { Fragment } from "react" +import cx from "classnames" import { MarkdownRemark } from "../../generated/graphql" import { ComponentQuery } from "../../typings" import { Markdown } from "../components/Markdown" import { SEO } from "../components/SEO" import { PageContent } from "../components/PageContent" import { HeaderBarebone } from "../components/HeaderBarebone" +import { buildToc } from "../utils" function AboutPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) { const { md } = data + const toc = buildToc(md.headings!) + return ( - + - } /> + } toc={toc} /> ) } @@ -24,6 +31,10 @@ export const query = graphql` query AboutPage { md: markdownRemark(frontmatter: { path: { eq: "/about" } }) { html + headings { + depth + value + } } } ` diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index 800770b..52e2d52 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -1,21 +1,25 @@ import { graphql } from "gatsby" import React, { Fragment } from "react" +import cx from "classnames" import { MarkdownRemark } from "../../generated/graphql" import { ComponentQuery } from "../../typings" import { HeaderBarebone } from "../components/HeaderBarebone" import { Markdown } from "../components/Markdown" import { PageContent } from "../components/PageContent" import { SEO } from "../components/SEO" +import { buildToc } from "../utils" function RulesPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) { const { md } = data + const toc = buildToc(md.headings!) + return ( - + - } /> + } toc={toc} /> ) } @@ -24,6 +28,10 @@ export const query = graphql` query RulesPage { md: markdownRemark(frontmatter: { path: { eq: "/rules" } }) { html + headings { + depth + value + } } } ` diff --git a/src/templates/languagePost.tsx b/src/templates/languagePost.tsx index 686b16c..3105044 100644 --- a/src/templates/languagePost.tsx +++ b/src/templates/languagePost.tsx @@ -1,20 +1,32 @@ import { graphql } from "gatsby" import React, { FC, Fragment } from "react" +import "katex/dist/katex.min.css" + +import { Footer } from "../components/Footer" import { Header } from "../components/Header" import { Markdown } from "../components/Markdown" -import { SEO } from "../components/SEO" - -import "katex/dist/katex.min.css" import { PageContent } from "../components/PageContent" -import { Footer } from "../components/Footer" +import { SEO } from "../components/SEO" +import { buildToc } from "../utils" // @todo maybe find alternative type for data const LanguagePost: FC = ({ data }) => { const { relativePath } = data.file - const { html, excerpt, fields, frontmatter, timeToRead } = data.file.post + const { + html, + headings, + excerpt, + fields, + frontmatter, + timeToRead, + } = data.file.post + + const toc = buildToc(headings) const shiftLayout = Boolean( - frontmatter.recommended_reading || frontmatter.external_resources + frontmatter.recommended_reading || + frontmatter.external_resources || + toc.length ) return ( @@ -36,6 +48,7 @@ const LanguagePost: FC = ({ data }) => {