diff --git a/src/components/ResourcesList/index.tsx b/src/components/ResourcesList/index.tsx new file mode 100644 index 0000000..72ad9f6 --- /dev/null +++ b/src/components/ResourcesList/index.tsx @@ -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 ( + + {cleanedUpPath.map(node => ( + // TODO: use helper to format path + {node.replace(".md", "")} + ))} + + ) + } + + return +} + +const Language = memo(({ item }: { item: IFolder; index: number }) => { + return ( + + {item.title} + {item.children.map(node => plantTree(node))} + + ) +}) + +export function ResourcesList(props: React.HTMLAttributes) { + const resources = useStaticQuery(ALL_RESOURCES) + const tree = useBuildTree(resources) + + return ( + + {tree.map((node, index) => plantTree(node, index))} + + ) +} diff --git a/src/components/ResourcesList/styles.tsx b/src/components/ResourcesList/styles.tsx new file mode 100644 index 0000000..156d03d --- /dev/null +++ b/src/components/ResourcesList/styles.tsx @@ -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; + } +` diff --git a/src/components/ResourcesList/useBuildTree.tsx b/src/components/ResourcesList/useBuildTree.tsx new file mode 100644 index 0000000..5adc513 --- /dev/null +++ b/src/components/ResourcesList/useBuildTree.tsx @@ -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) +} diff --git a/src/pages/resources.tsx b/src/pages/resources.tsx index c171d75..1f8ec2c 100644 --- a/src/pages/resources.tsx +++ b/src/pages/resources.tsx @@ -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 ( - the resources +

Welcome to the TPH resources.

+

+ This is meant as a small knowledge base for commonly answered questions + on our Discord community,{" "} + The Programmer's Hangout. +

+
) } diff --git a/src/utils/index.ts b/src/utils/index.ts index a7a3fd2..0a9c680 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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)], + } +}