mirror of
https://github.com/Stone-Red-Code/website.git
synced 2026-09-07 07:56:12 +02:00
Merge pull request #92 from the-programmers-hangout/feat-resources-entry-pages
feat(resources) entry pages
This commit is contained in:
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
`
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -4,44 +4,12 @@ import Scrollbar from "react-perfect-scrollbar"
|
|||||||
import "react-perfect-scrollbar/dist/css/styles.css"
|
import "react-perfect-scrollbar/dist/css/styles.css"
|
||||||
import TriangleDown from "../../icons/triangle-down.svg"
|
import TriangleDown from "../../icons/triangle-down.svg"
|
||||||
import Banner from "../../images/tph-banner.svg"
|
import Banner from "../../images/tph-banner.svg"
|
||||||
|
import { IAllResourcesQuery, IFileOrFolder, IFolder } from "../../types"
|
||||||
import { ThemeToggler } from "../ThemeToggler"
|
import { ThemeToggler } from "../ThemeToggler"
|
||||||
import useSidebar from "./../../hooks/useSidebar"
|
import useSidebar from "./../../hooks/useSidebar"
|
||||||
import * as SC from "./styles"
|
import * as SC from "./styles"
|
||||||
import useBuildTree from "./useBuildTree"
|
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`
|
const ALL_RESOURCES = graphql`
|
||||||
query {
|
query {
|
||||||
allFile(filter: { sourceInstanceName: { eq: "resources" } }) {
|
allFile(filter: { sourceInstanceName: { eq: "resources" } }) {
|
||||||
|
|||||||
@@ -1,67 +1,5 @@
|
|||||||
import chain from "ramda/es/chain"
|
import { IAllResourcesQuery, IFileQuery } from "../../types"
|
||||||
import partition from "ramda/es/partition"
|
import { join, traversePaths } from "../../utils"
|
||||||
|
|
||||||
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)]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useBuildTree(resources: IAllResourcesQuery) {
|
export default function useBuildTree(resources: IAllResourcesQuery) {
|
||||||
const objects = resources.allFile.edges.map(({ node: file }: IFileQuery) =>
|
const objects = resources.allFile.edges.map(({ node: file }: IFileQuery) =>
|
||||||
|
|||||||
+14
-1
@@ -1,12 +1,25 @@
|
|||||||
import React, { Fragment } from "react"
|
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"
|
import { SEO } from "../components/SEO"
|
||||||
|
|
||||||
function ResourcesPage() {
|
function ResourcesPage() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SEO title="Resources" />
|
<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>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+32
@@ -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
@@ -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(
|
export function traversePaths(
|
||||||
[head, ...tail]: string[],
|
[head, ...tail]: string[],
|
||||||
@@ -22,3 +25,85 @@ export function traversePaths(
|
|||||||
children: [traversePaths(tail, path)],
|
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
@@ -46,7 +46,7 @@
|
|||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
// "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. */
|
"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. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
@@ -59,5 +59,6 @@
|
|||||||
/* Experimental Options */
|
/* Experimental Options */
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -3,6 +3,7 @@ import "styled-components"
|
|||||||
|
|
||||||
declare module "*.svg"
|
declare module "*.svg"
|
||||||
declare module "*.png"
|
declare module "*.png"
|
||||||
|
declare module "*.json"
|
||||||
|
|
||||||
type ComponentQuery<T> = {
|
type ComponentQuery<T> = {
|
||||||
readonly data: T
|
readonly data: T
|
||||||
|
|||||||
Reference in New Issue
Block a user