mirror of
https://github.com/Stone-Red-Code/website.git
synced 2026-09-04 09:15:58 +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 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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
[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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ import "styled-components"
|
||||
|
||||
declare module "*.svg"
|
||||
declare module "*.png"
|
||||
declare module "*.json"
|
||||
|
||||
type ComponentQuery<T> = {
|
||||
readonly data: T
|
||||
|
||||
Reference in New Issue
Block a user