Merge pull request #241 from the-programmers-hangout/feat/toc

feat: support table of contents in resources, about, and rules
This commit is contained in:
Jean-Philippe Sirois
2020-05-12 00:16:39 -04:00
committed by GitHub
13 changed files with 201 additions and 38 deletions
+6
View File
@@ -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`,
+24
View File
@@ -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",
+1
View File
@@ -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",
+12 -1
View File
@@ -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<IPageContentProps> = ({
content,
toc = [],
recommendedReading,
externalResources,
}) => {
@@ -52,6 +56,13 @@ export const PageContent: FC<IPageContentProps> = ({
</SC.Content>
<SC.Sidebar>
{toc.length > 0 && (
<Toc
header={<SC.SidebarHeader>Table of Contents</SC.SidebarHeader>}
items={toc}
/>
)}
{recommendedReading && (
<SC.RecommendedReading>
<SC.SidebarHeader>Recommended reading</SC.SidebarHeader>
+7 -1
View File
@@ -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)``
+49
View File
@@ -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<ITocProps> = ({ header, items }) => {
const windowGlobal = typeof window !== "undefined" && window
// window is not available on build
if (!windowGlobal) {
return null
}
const { pathname } = windowGlobal.location
return (
<SC.TocWrapper>
{header}
{items.map(item => {
const { prefix, title } = extractTitle(item.title)
return (
<SC.TocItem key={item.link} className={`depth-${item.depth}`}>
{prefix}
<SC.TocLink to={`${pathname}${item.link}`}>{title}</SC.TocLink>
</SC.TocItem>
)
})}
</SC.TocWrapper>
)
}
+30
View File
@@ -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;
}
`
-24
View File
@@ -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)
+13 -2
View File
@@ -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 (
<Fragment>
<SEO title="About" />
<HeaderBarebone title="About us" />
<HeaderBarebone
title="About us"
className={cx({ shifted: toc.length })}
/>
<PageContent content={<Markdown content={md.html!} />} />
<PageContent content={<Markdown content={md.html!} />} toc={toc} />
</Fragment>
)
}
@@ -24,6 +31,10 @@ export const query = graphql`
query AboutPage {
md: markdownRemark(frontmatter: { path: { eq: "/about" } }) {
html
headings {
depth
value
}
}
}
`
+10 -2
View File
@@ -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 (
<Fragment>
<SEO title="Rules" />
<HeaderBarebone title="Rules" />
<HeaderBarebone title="Rules" className={cx({ shifted: toc.length })} />
<PageContent content={<Markdown content={md.html!} />} />
<PageContent content={<Markdown content={md.html!} />} toc={toc} />
</Fragment>
)
}
@@ -24,6 +28,10 @@ export const query = graphql`
query RulesPage {
md: markdownRemark(frontmatter: { path: { eq: "/rules" } }) {
html
headings {
depth
value
}
}
}
`
+23 -6
View File
@@ -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<any> = ({ 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<any> = ({ data }) => {
<Footer />
</>
}
toc={toc}
recommendedReading={frontmatter.recommended_reading}
externalResources={frontmatter.external_resources}
/>
@@ -51,6 +64,10 @@ export const query = graphql`
relativePath
post: childMarkdownRemark {
html
headings {
depth
value
}
excerpt
fields {
authors {
+6
View File
@@ -55,3 +55,9 @@ export interface IAllArchivesQuery extends IAllFilesQuery {
edges: IFileArchiveQuery[]
}
}
export interface ITocItem {
depth: number
link: string
title: string
}
+20 -2
View File
@@ -1,9 +1,11 @@
import kebabCase from "lodash/kebabCase"
import chain from "ramda/es/chain"
import partition from "ramda/es/partition"
import pipe from "ramda/es/pipe"
import sort from "ramda/es/sort"
import { sort } from "ramda"
import { IFile, IFileOrFolder, IFolder } from "../types"
import { IFile, IFileOrFolder, IFolder, ITocItem } from "../types"
import { MarkdownRemark } from "../../generated/graphql"
function specificWordsToUpper(str: string): string {
const wordsToUpper = ["pdo", "c"]
@@ -139,3 +141,19 @@ export function join([head, ...tail]: IFileOrFolder[]): IFileOrFolder[] {
return [current, ...join(remaining)]
}
export function buildToc(headings: MarkdownRemark["headings"]): ITocItem[] {
if (!headings) {
return []
}
return headings
.filter(h => h?.depth === 2)
.map(h => {
return {
depth: h!.depth!,
link: `#${kebabCase(h!.value!)}`,
title: h!.value!,
}
})
}