Merge pull request #92 from the-programmers-hangout/feat-resources-entry-pages

feat(resources) entry pages
This commit is contained in:
Jean-Philippe Sirois
2019-09-17 02:00:24 -04:00
committed by GitHub
12 changed files with 358 additions and 100 deletions
+21
View File
@@ -0,0 +1,21 @@
import React, { PropsWithChildren } from "react"
import * as SC from "./styles"
export function ResourcesLink({
children,
to,
}: PropsWithChildren<{
to: string
}>) {
if (!to.match(/^(https?:\/\/)/)) {
return (
<SC.ResourcesLinkInternal to={to}>{children}</SC.ResourcesLinkInternal>
)
}
return (
<SC.ResourcesLinkExternal href={to} target="_blank">
{children}
</SC.ResourcesLinkExternal>
)
}
+24
View File
@@ -0,0 +1,24 @@
import { Link } from "gatsby"
import styled, { css } from "styled-components"
const linkStyle = css`
color: #0090d8;
font-weight: 700;
border-bottom: 2px solid;
text-decoration: none;
transition: color 0.3s;
&:hover,
&:focus {
color: #5dbbea;
transition: none;
}
`
export const ResourcesLinkInternal = styled(Link)`
${linkStyle}
`
export const ResourcesLinkExternal = styled.a`
${linkStyle}
`
+62
View File
@@ -0,0 +1,62 @@
import { graphql, useStaticQuery } from "gatsby"
import React, { memo } from "react"
import "react-perfect-scrollbar/dist/css/styles.css"
import { IAllResourcesQuery, IFileOrFolder, IFolder } from "../../types"
import * as SC from "./styles"
import useBuildTree from "./useBuildTree"
const ALL_RESOURCES = graphql`
query {
allFile(filter: { sourceInstanceName: { eq: "resources" } }) {
edges {
node {
relativePath
childMarkdownRemark {
frontmatter {
authors
title
date
}
}
}
}
}
}
`
function plantTree(item: IFileOrFolder, index?: number) {
if (item.type === "file") {
// remove the first elements, treat as hardcoded
const [, , , ...cleanedUpPath] = item.path.split("/")
return (
<SC.PageLink key={item.title} to={item.path}>
{cleanedUpPath.map(node => (
// TODO: use helper to format path
<SC.NodePart key={node}>{node.replace(".md", "")}</SC.NodePart>
))}
</SC.PageLink>
)
}
return <Language key={item.title} item={item} index={index!} />
}
const Language = memo(({ item }: { item: IFolder; index: number }) => {
return (
<SC.TreeWrapper>
<SC.LanguageLabel>{item.title}</SC.LanguageLabel>
<SC.Children>{item.children.map(node => plantTree(node))}</SC.Children>
</SC.TreeWrapper>
)
})
export function ResourcesList(props: React.HTMLAttributes<HTMLDivElement>) {
const resources = useStaticQuery<IAllResourcesQuery>(ALL_RESOURCES)
const tree = useBuildTree(resources)
return (
<SC.ResourcesListWrapper {...props}>
{tree.map((node, index) => plantTree(node, index))}
</SC.ResourcesListWrapper>
)
}
+103
View File
@@ -0,0 +1,103 @@
import { Link } from "gatsby"
import { transparentize } from "polished"
import styled from "styled-components"
export const ResourcesListWrapper = styled.div`
box-sizing: border-box;
color: ${props => props.theme.main.foreground};
`
export const Children = styled.div`
padding-left: 16px;
padding-bottom: 8px;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
`
export const Label = styled.div`
user-select: none;
position: relative;
padding: 4px 0 2px;
margin-bottom: 2px;
cursor: pointer;
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
align-self: flex-start;
&:hover {
border-color: ${props =>
transparentize(0.7, props.theme.sidebar.foreground)};
}
svg {
position: absolute;
right: 100%;
margin-right: 4px;
width: 10px;
}
`
export const LanguageLabel = styled.div`
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 4px 15px 4px 0;
font-weight: 700;
color: ${props => transparentize(0.5, props.theme.sidebar.foreground)};
svg {
margin-left: auto;
transform: rotate(90deg);
transition: transform 0.3s;
path {
fill: ${props => transparentize(0.5, props.theme.sidebar.foreground)};
}
}
`
export const TreeWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
font-size: 16px;
margin: 32px 0;
${Children} {
display: flex;
}
& > ${Label} svg {
fill: ${props => props.theme.sidebar.foreground};
}
`
export const PageLink = styled(Link)`
display: block;
padding: 2px 0;
color: ${props => props.theme.sidebar.foreground};
align-self: flex-start;
text-decoration: none;
border-bottom: 1px solid transparent;
&:hover {
border-color: ${props =>
transparentize(0.7, props.theme.sidebar.foreground)};
}
& + & {
margin-top: 4px;
}
`
export const NodePart = styled.span`
& + &::before {
content: "/";
opacity: 0.3;
margin: 0 4px;
}
`
@@ -0,0 +1,10 @@
import { IAllResourcesQuery, IFileQuery } from "../../types"
import { join, traversePathsToFiles } from "../../utils"
export default function useBuildTree(resources: IAllResourcesQuery) {
const objects = resources.allFile.edges.map(({ node: file }: IFileQuery) =>
traversePathsToFiles(file.relativePath.split("/"))
)
return join(objects)
}
+1 -33
View File
@@ -4,44 +4,12 @@ import Scrollbar from "react-perfect-scrollbar"
import "react-perfect-scrollbar/dist/css/styles.css"
import TriangleDown from "../../icons/triangle-down.svg"
import Banner from "../../images/tph-banner.svg"
import { IAllResourcesQuery, IFileOrFolder, IFolder } from "../../types"
import { ThemeToggler } from "../ThemeToggler"
import useSidebar from "./../../hooks/useSidebar"
import * as SC from "./styles"
import useBuildTree from "./useBuildTree"
export interface IFile {
title: string
type: "file"
path: string
}
export interface IFolder {
title: string
type: "folder"
path: string
children: IFileOrFolder[]
}
export type IFileOrFolder = IFile | IFolder
export interface IFileQuery {
node: {
relativePath: string
childMarkdownRemark: {
frontmatter: {
author: string
date: string
}
}
}
}
export interface IAllResourcesQuery {
allFile: {
edges: IFileQuery[]
}
}
const ALL_RESOURCES = graphql`
query {
allFile(filter: { sourceInstanceName: { eq: "resources" } }) {
@@ -1,67 +1,5 @@
import chain from "ramda/es/chain"
import partition from "ramda/es/partition"
import { traversePaths } from "../../utils"
import {
IAllResourcesQuery,
IFile,
IFileOrFolder,
IFileQuery,
IFolder,
} from "./index"
function generateFolder({
title,
path,
targets,
}: {
title: IFolder["title"]
path: IFile["path"]
targets: IFolder[]
}): IFolder {
const children = join(chain(target => target.children, targets))
return {
title,
type: "folder",
path,
children,
}
}
function generateFile({
title,
path,
}: {
title: IFile["title"]
path: IFile["path"]
}): IFile {
return {
title,
type: "file",
path,
}
}
function join([head, ...tail]: IFileOrFolder[]): IFileOrFolder[] {
if (!head) {
return []
}
const [similarFs, remaining] = partition(
obj => obj.title === head.title && obj.type === head.type,
tail
)
const targets = [head, ...similarFs]
const { title, path } = head
const current =
head.type === "folder"
? generateFolder({ title, path, targets: targets as IFolder[] })
: generateFile({ title, path })
return [current, ...join(remaining)]
}
import { IAllResourcesQuery, IFileQuery } from "../../types"
import { join, traversePaths } from "../../utils"
export default function useBuildTree(resources: IAllResourcesQuery) {
const objects = resources.allFile.edges.map(({ node: file }: IFileQuery) =>
+14 -1
View File
@@ -1,12 +1,25 @@
import React, { Fragment } from "react"
import packageJson from "../../package.json"
import { ResourcesLink } from "../components/ResourcesLink"
import { ResourcesList } from "../components/ResourcesList"
import { SEO } from "../components/SEO"
function ResourcesPage() {
return (
<Fragment>
<SEO title="Resources" />
the resources
<p>Welcome to the TPH resources.</p>
<p>
This is meant as a small knowledge base for commonly answered questions
on our Discord community,{" "}
<ResourcesLink to="/about">The Programmer's Hangout</ResourcesLink>.
</p>
<p>
All of it is open source, and you can contribute to it on{" "}
<ResourcesLink to={packageJson.repository.url}>GitHub</ResourcesLink>.
</p>
<ResourcesList />
</Fragment>
)
}
+32
View File
@@ -0,0 +1,32 @@
export interface IFile {
title: string
type: "file"
path: string
}
export interface IFolder {
title: string
type: "folder"
path: string
children: IFileOrFolder[]
}
export type IFileOrFolder = IFile | IFolder
export interface IFileQuery {
node: {
relativePath: string
childMarkdownRemark: {
frontmatter: {
author: string
date: string
}
}
}
}
export interface IAllResourcesQuery {
allFile: {
edges: IFileQuery[]
}
}
+86 -1
View File
@@ -1,4 +1,7 @@
import { IFileOrFolder } from "../components/ResourcesSidebar/index"
import chain from "ramda/es/chain"
import partition from "ramda/es/partition"
import { IFile, IFileOrFolder, IFolder } from "../types"
export function traversePaths(
[head, ...tail]: string[],
@@ -22,3 +25,85 @@ export function traversePaths(
children: [traversePaths(tail, path)],
}
}
export function traversePathsToFiles(
[head, ...tail]: string[],
basePath: string = "/resources",
depth: number = 0
): IFileOrFolder {
const path = basePath + "/" + head + "/" + tail.join("/")
if (depth !== 0) {
// probably not more than one dot
const [name] = basePath.split(".")
return {
title: name,
type: "file",
path: basePath,
}
}
return {
title: head,
type: "folder",
path,
children: [traversePathsToFiles(tail, path, 1)],
}
}
function generateFolder({
title,
path,
targets,
}: {
title: IFolder["title"]
path: IFile["path"]
targets: IFolder[]
}): IFolder {
const children = join(chain(target => target.children, targets))
return {
title,
type: "folder",
path,
children: children.sort((a, b) =>
a.title.split("/").length > b.title.split("/").length
? -1
: a.title.localeCompare(b.title)
),
}
}
function generateFile({
title,
path,
}: {
title: IFile["title"]
path: IFile["path"]
}): IFile {
return {
title,
type: "file",
path,
}
}
export function join([head, ...tail]: IFileOrFolder[]): IFileOrFolder[] {
if (!head) {
return []
}
const [similarFs, remaining] = partition(
obj => obj.title === head.title && obj.type === head.type,
tail
)
const targets = [head, ...similarFs]
const { title, path } = head
const current =
head.type === "folder"
? generateFolder({ title, path, targets: targets as IFolder[] })
: generateFile({ title, path })
return [current, ...join(remaining)]
}
+2 -1
View File
@@ -46,7 +46,7 @@
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@@ -59,5 +59,6 @@
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"resolveJsonModule": true
}
}
+1
View File
@@ -3,6 +3,7 @@ import "styled-components"
declare module "*.svg"
declare module "*.png"
declare module "*.json"
type ComponentQuery<T> = {
readonly data: T