diff --git a/src/components/ResourcesLink/index.tsx b/src/components/ResourcesLink/index.tsx new file mode 100644 index 0000000..60b4055 --- /dev/null +++ b/src/components/ResourcesLink/index.tsx @@ -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 ( + {children} + ) + } + + return ( + + {children} + + ) +} diff --git a/src/components/ResourcesLink/styles.tsx b/src/components/ResourcesLink/styles.tsx new file mode 100644 index 0000000..27a0eee --- /dev/null +++ b/src/components/ResourcesLink/styles.tsx @@ -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} +` diff --git a/src/components/ResourcesList/index.tsx b/src/components/ResourcesList/index.tsx new file mode 100644 index 0000000..7109ad1 --- /dev/null +++ b/src/components/ResourcesList/index.tsx @@ -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 ( + + {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..f347827 --- /dev/null +++ b/src/components/ResourcesList/useBuildTree.tsx @@ -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) +} diff --git a/src/components/ResourcesSidebar/index.tsx b/src/components/ResourcesSidebar/index.tsx index 0dca75c..75b82ad 100644 --- a/src/components/ResourcesSidebar/index.tsx +++ b/src/components/ResourcesSidebar/index.tsx @@ -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" } }) { diff --git a/src/components/ResourcesSidebar/useBuildTree.tsx b/src/components/ResourcesSidebar/useBuildTree.tsx index b038b2f..0be532b 100644 --- a/src/components/ResourcesSidebar/useBuildTree.tsx +++ b/src/components/ResourcesSidebar/useBuildTree.tsx @@ -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) => diff --git a/src/pages/resources.tsx b/src/pages/resources.tsx index c171d75..9b062e4 100644 --- a/src/pages/resources.tsx +++ b/src/pages/resources.tsx @@ -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 ( - 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. +

+

+ All of it is open source, and you can contribute to it on{" "} + GitHub. +

+
) } diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..5d1a69c --- /dev/null +++ b/src/types/index.d.ts @@ -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[] + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index a7a3fd2..0f12043 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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)] +} diff --git a/tsconfig.json b/tsconfig.json index 3565c9a..8249c01 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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 } } diff --git a/typings.d.ts b/typings.d.ts index 3a47d73..04494d2 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -3,6 +3,7 @@ import "styled-components" declare module "*.svg" declare module "*.png" +declare module "*.json" type ComponentQuery = { readonly data: T