resources: set up home page

This commit is contained in:
Jean-Philippe Sirois
2019-09-17 01:58:15 -04:00
parent 2d16e9489c
commit 5bd0787112
5 changed files with 308 additions and 2 deletions
+94
View File
@@ -0,0 +1,94 @@
import { graphql, useStaticQuery } from "gatsby"
import React, { memo } from "react"
import "react-perfect-scrollbar/dist/css/styles.css"
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" } }) {
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,76 @@
import chain from "ramda/es/chain"
import partition from "ramda/es/partition"
import { traversePathsToFiles } 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: 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,
}
}
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)]
}
export default function useBuildTree(resources: IAllResourcesQuery) {
const objects = resources.allFile.edges.map(({ node: file }: IFileQuery) =>
traversePathsToFiles(file.relativePath.split("/"))
)
return join(objects)
}
+9 -1
View File
@@ -1,12 +1,20 @@
import { Link } from "gatsby"
import React, { Fragment } from "react"
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,{" "}
<Link to="/about">The Programmer's Hangout</Link>.
</p>
<ResourcesList />
</Fragment>
)
}
+26 -1
View File
@@ -1,4 +1,4 @@
import { IFileOrFolder } from "../components/ResourcesSidebar/index"
import { IFile, IFileOrFolder } from "../components/ResourcesSidebar/index"
export function traversePaths(
[head, ...tail]: string[],
@@ -22,3 +22,28 @@ 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)],
}
}