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..06350f5 --- /dev/null +++ b/src/components/Toc/index.tsx @@ -0,0 +1,49 @@ +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 windowGlobal = typeof window !== "undefined" && window + + // window is not available on build + if (!windowGlobal) { + return null + } + + const { pathname } = windowGlobal.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/content/site/rules.md b/src/content/site/rules.md index 84fd143..a469659 100644 --- a/src/content/site/rules.md +++ b/src/content/site/rules.md @@ -2,16 +2,6 @@ path: /rules --- -# Table of Contents - -- [General Rules](#general-rules) -- [Golden Rules](#golden-rules) -- [Content Policy](#content-policy) -- [Why isn't security talk allowed?](#why-dont-you-allow-talk-regarding-hacking) -- [Server Roles](#roles) -- [Nickname Policy](#nickname-policy) -- [What is Modmail / How to contact staff](#what-is-modmail) - ## General Rules Below you will find the rules of the server. These are the dos and don'ts of being here. Follow them if you wish to stay, ignorance is not an excuse. @@ -76,8 +66,6 @@ Bans on all linked servers are propagated. This means that if you are banned on The FAQ and beginner guide channels represent many hours of discussion to find well-rounded answers to common questions. Please link to them regularly when beginners and the uninformed ask those questions. You may be asked to cease your participation in a conversation if you present yourself as a zealot with distracting opinions. -[Table of Contents](#table-of-contents) - ## Golden Rules ### 1. Don't be a pedant. @@ -98,8 +86,6 @@ Treat other members, regardless of their skill level, as though you have somethi In general, if you have a contribution to make, _please be encouraging_. Being pedantic or gatekeeping how "hard" something is or pretending like you're some kind of God when it comes to programming is purely reductive garbage. If you are found to be guilty of violating these tenants appropriate action shall be taken against you. There are far too many people out there actively discouraging self-improvement and learning. This server refuses to stand for it. -[Table of Contents](#table-of-contents) - ## Content Policy Below is a list of things that you cannot talk about here. If a staff member cites you the content policy, please understand that they really mean _stop_ talking about the thing you are talking about. I think you will find the list reasonable, as we construct it to be conducive to an environment that suits talking about programming and technology in the absence of distractions. @@ -128,8 +114,6 @@ As annoying as it is for me to write this, do not ask about how to "get the girl We do not care that you got banned on another server. Sorry, but not our problem. Take it up with that server's staff. -[Table of Contents](#table-of-contents) - ## Why don't you allow talk regarding hacking? There are a few reasons for not allowing it: - It is directly against the Discord Terms of Service (ToS), allowing talk of how to perform hacks is basically never going to be allowed for this reason. @@ -146,8 +130,6 @@ Light talk about prevention is allowed. Providing elaborate details in any capac Try not to, honestly, if you are looking to prevent a specific kind of attack feel free to ask if your code prevents that, outside of that sharing details as to how you might get past some code isn't the best idea, try to talk about the solution more so than the gory details of the problem. Feel free to share the gory details via DM, but don't wait to take the conversation to DM. This is one of the very few topic areas we want you to take it to DM, since information spread in this area is not something we want to prevent, rather, the discussion around it. -[Table of Contents](#table-of-contents) - ## Roles TPH has a relatively small amount of roles, but each of our roles has a purpose. We don't have 30 language roles and a staff rank. That being said, it's kind of tricky to keep a track of all of the roles at once - so here you can see what they're for. @@ -172,8 +154,6 @@ This role recognizes people who contribute their time and knowledge to the serve Self-explanatory - _ChatMod/Moderator/Senior Moderator/Admin/Owner_ - These people ensure that the usage of this environment reflects the guidelines as illustrated in the rules. Staff is occasionally handpicked, and sometimes there is an application process. There is a trial period for all new staff of 2-4 weeks (Sometimes more, sometimes less) which comes with a trial role, however, they are treated as full staff members during this time. Please respect all staff equally; if you have an issue with a specific staff member, talk to an administrator. -[Table of Contents](#table-of-contents) - ## Nickname Policy On TPH, we enforce a nickname policy - This means that if your nickname is deemed too NSFW (not safe for work), or hard to read/tag (it uses weird characters, it's blank or it contains numbers) we can and will forcibly nickname you to something else. @@ -184,8 +164,6 @@ _This is harmless fun, if you just accept it, laugh and move on, no problem._ If you pipe up and get annoyed and flustered that you don't have your name, you're gonna have a bad time. Everyone, including the owner, follows nickname trends and jokes. This is in an attempt to be inclusive, and most people who stay here either genuinely don't care about it, or quite enjoy it. It's a mere 10% (yes, we have actually polled for this) that dislike it, and much less actually leave because of it. Don't be the sour apple if you get nicknamed something funny. -[Table of Contents](#table-of-contents) - ## What is ModMail? ModMail (@ModMail) is a bot that reports every direct message received to staff. @@ -204,5 +182,3 @@ Send it a message. This will open a two-way communication medium between you and ### Don't use it when you... Notice something which requires immediate attention such as raids or NSFW content. In these cases, use the serious rule break tag instead to immediately alert staff. - -[Table of Contents](#table-of-contents) 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 }) => {