feat: style resources page (#30)

feat: style resources sidebar
This commit is contained in:
Jean-Philippe Sirois
2019-07-30 11:34:03 -04:00
20 changed files with 401 additions and 113 deletions
+35
View File
@@ -0,0 +1,35 @@
import React, { useState } from "react"
interface SidebarProviderProps {
children: React.ReactNode
}
export interface SidebarContextInterface {
current: number
setCurrent: (index: number) => void
}
export const SidebarContext = React.createContext<SidebarContextInterface | null>(
null
)
export function SidebarProvider({
children,
}: SidebarProviderProps): JSX.Element {
const [current, setCurrent] = useState(0)
function selectFirstLevel(index: number) {
setCurrent(index)
}
return (
<SidebarContext.Provider
value={{
current,
setCurrent: selectFirstLevel,
}}
>
{children}
</SidebarContext.Provider>
)
}
+17 -18
View File
@@ -2,21 +2,12 @@ import styled, { css } from "styled-components"
import { import {
BASE_FONT_SIZE, BASE_FONT_SIZE,
BASE_LINE_HEIGHT, BASE_LINE_HEIGHT,
MODULAR_SCALE,
fontFamily, fontFamily,
modularScale,
} from "../../design/typography" } from "../../design/typography"
function modularFontSize(power: number) { function modularScaleCSS(power: number) {
return BASE_FONT_SIZE * Math.pow(MODULAR_SCALE, power) const { fontSize, lineHeight } = modularScale(power)
}
function modularScale(power: number) {
const fontSize = modularFontSize(power)
// attempt to fit line-height a little larger than font-size
const paddedLineHeight = fontSize * 1.1
const lineHeight =
paddedLineHeight - (paddedLineHeight % BASE_LINE_HEIGHT) + BASE_LINE_HEIGHT
return css` return css`
font-size: ${fontSize}px; font-size: ${fontSize}px;
@@ -28,6 +19,14 @@ export const MarkdownWrapper = styled.div`
font-size: ${BASE_FONT_SIZE}px; font-size: ${BASE_FONT_SIZE}px;
line-height: ${BASE_LINE_HEIGHT}px; line-height: ${BASE_LINE_HEIGHT}px;
& > :first-child {
margin-top: 0;
}
& > :first-child {
margin-bottom: 0;
}
p { p {
margin: ${BASE_LINE_HEIGHT}px 0; margin: ${BASE_LINE_HEIGHT}px 0;
} }
@@ -45,26 +44,26 @@ export const MarkdownWrapper = styled.div`
} }
h1 { h1 {
${modularScale(6)}; ${modularScaleCSS(6)};
} }
h2 { h2 {
${modularScale(5)}; ${modularScaleCSS(5)};
} }
h3 { h3 {
${modularScale(4)}; ${modularScaleCSS(4)};
} }
h4 { h4 {
${modularScale(3)}; ${modularScaleCSS(3)};
} }
h5 { h5 {
${modularScale(2)}; ${modularScaleCSS(2)};
} }
h6 { h6 {
${modularScale(1)}; ${modularScaleCSS(1)};
} }
` `
+60
View File
@@ -0,0 +1,60 @@
import React from "react"
import ChevronUp from "../../icons/chevron-up.svg"
import * as SC from "./styles"
interface ResourceHeaderProps {
title: any
authors: any
createdAt: any
timeToRead: any
recommendedReading: any
}
export function ResourceHeader({
title,
authors,
createdAt,
timeToRead,
recommendedReading,
}: ResourceHeaderProps) {
const date = new Date(createdAt)
const month = date.toLocaleString("default", { month: "long" })
const day = date.getDate()
const year = date.getUTCFullYear()
const dateToHuman = `${month} ${day}, ${year}`
return (
<SC.ResourceHeaderWrapper>
<SC.Title>{title}</SC.Title>
<SC.Top>
<SC.AuthorAvatars>
{authors.map(author => (
<SC.AuthorAvatar src={author.avatar} />
))}
</SC.AuthorAvatars>
<SC.Meta>
{authors.length} contributor{authors.lenght > 1 && "s"}
</SC.Meta>
<SC.Meta>{dateToHuman}</SC.Meta>
<SC.Meta>
{timeToRead} minute{timeToRead !== 1 && "s"} read time
</SC.Meta>
</SC.Top>
{recommendedReading && (
<SC.RecommendedReading>
Recommended reading
{recommendedReading.map(item => {
return (
<SC.ReadLink>
<ChevronUp /> <SC.ReadLinkText>{item}</SC.ReadLinkText>
</SC.ReadLink>
)
})}
</SC.RecommendedReading>
)}
</SC.ResourceHeaderWrapper>
)
}
+69
View File
@@ -0,0 +1,69 @@
import styled from "styled-components"
import { fontFamily, modularScale } from "../../design/typography"
export const ResourceHeaderWrapper = styled.div`
border-bottom: 1px solid #dbdbdb;
padding-bottom: 32px;
margin-bottom: 32px;
`
export const Top = styled.div`
display: flex;
align-items: center;
`
export const Title = styled.h1`
font-family: ${fontFamily.header};
font-size: ${modularScale(6).fontSize}px;
line-height: ${modularScale(6).lineHeight}px;
letter-spacing: -1.75px;
`
export const AuthorAvatars = styled.div`
margin-right: 16px;
`
export const AuthorAvatar = styled.img`
width: 32px;
height: 32px;
border: 2px solid #fff;
border-radius: 50%;
`
export const Meta = styled.div`
& + &::before {
content: "•";
margin: 0 8px;
}
`
export const RecommendedReading = styled.div`
margin-top: 16px;
`
export const ReadLink = styled.div`
display: flex;
align-items: center;
color: #04b0a6;
margin-top: 4px;
margin-left: 20px;
cursor: pointer;
&:hover {
color: #045551;
}
svg {
width: 14px;
transform: rotate(90deg);
margin-right: 6px;
}
svg path {
fill: #04b0a6;
}
`
export const ReadLinkText = styled.div`
text-decoration: underline;
`
+4 -17
View File
@@ -6,35 +6,22 @@
*/ */
import React, { PropsWithChildren } from "react" import React, { PropsWithChildren } from "react"
import { useStaticQuery, graphql } from "gatsby"
import { GlobalStyles } from "../../globalStyles" import { GlobalStyles } from "../../globalStyles"
import { ResourcesSidebar } from "../ResourcesSidebar" import { ResourcesSidebar } from "../ResourcesSidebar"
import { Footer } from "../Footer"
import * as SC from "./styles" import * as SC from "./styles"
import { SidebarProvider } from "../../SidebarProvider"
export function ResourcesLayout({ children }: PropsWithChildren<{}>) { export function ResourcesLayout({ children }: PropsWithChildren<{}>) {
const data = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
}
}
}
`)
return ( return (
<div> <SidebarProvider>
<GlobalStyles /> <GlobalStyles />
<SC.Main> <SC.Main>
<ResourcesSidebar /> <ResourcesSidebar />
<SC.MainContent> <SC.MainContent>
<h1>{data.site.siteMetadata.title}</h1> <SC.Container>{children}</SC.Container>
{children}
</SC.MainContent> </SC.MainContent>
</SC.Main> </SC.Main>
<Footer /> </SidebarProvider>
</div>
) )
} }
+7 -1
View File
@@ -2,12 +2,13 @@ import styled from "styled-components"
export const Main = styled.div` export const Main = styled.div`
display: flex; display: flex;
margin: 128px auto 64px !important;
width: 100%; width: 100%;
min-height: 100vh;
` `
export const MainContent = styled.main` export const MainContent = styled.main`
flex: 1 1 auto; flex: 1 1 auto;
margin-top: 128px;
& > :first-child { & > :first-child {
margin-top: 0; margin-top: 0;
@@ -17,3 +18,8 @@ export const MainContent = styled.main`
margin-bottom: 0; margin-bottom: 0;
} }
` `
export const Container = styled.div`
width: 650px;
margin: 0 auto;
`
+47 -11
View File
@@ -2,6 +2,8 @@ import React, { useState } from "react"
import Tree from "react-treeview" import Tree from "react-treeview"
import { useStaticQuery, graphql } from "gatsby" import { useStaticQuery, graphql } from "gatsby"
import useBuildTree from "./useBuildTree" import useBuildTree from "./useBuildTree"
import useSidebar from "./../../hooks/useSidebar"
import banner from "../../images/tph-banner.png"
import * as SC from "./styles" import * as SC from "./styles"
export interface IFile { export interface IFile {
@@ -56,32 +58,63 @@ const ALL_RESOURCES = graphql`
} }
` `
function plantTree(item: IFileOrFolder) { function plantTree(item: IFileOrFolder, index?: number, firstLevel?: boolean) {
if (item.type === "file") { if (item.type === "file") {
return ( return (
<SC.PageLink to={item.path} activeClassName="active"> <SC.PageLink key={item.title} to={item.path} activeClassName="active">
{item.title} {item.title}
</SC.PageLink> </SC.PageLink>
) )
} }
return <Folder item={item} /> if (firstLevel && index !== undefined) {
return <FirstLevelFolder key={item.title} item={item} index={index} />
}
return <Folder key={item.title} item={item} />
} }
function Folder({ item }: { item: IFolder }) { function Folder({ item }: { item: IFolder }) {
const [collapsed, setCollapse] = useState(false) const [collapsed, setCollapse] = useState(true)
function toggleCollapse() { function toggleCollapse() {
setCollapse(prevState => !prevState) setCollapse(prevState => !prevState)
} }
return ( return (
<Tree <SC.TreeWrapper>
collapsed={collapsed} <Tree
nodeLabel={<div onClick={toggleCollapse}>{item.title}</div>} collapsed={collapsed}
> nodeLabel={<SC.Label onClick={toggleCollapse}>{item.title}</SC.Label>}
{item.children.map(plantTree)} >
</Tree> {item.children.map(node => plantTree(node))}
</Tree>
</SC.TreeWrapper>
)
}
function FirstLevelFolder({ item, index }: { item: IFolder; index: number }) {
const { current, setCurrent } = useSidebar()
const collapsed = current !== index
return (
<SC.TreeWrapper>
<Tree
className="firstLevel"
collapsed={collapsed}
nodeLabel={
<SC.FirstLabel
className={!collapsed ? "active" : undefined}
onClick={() => setCurrent(index)}
>
{item.title}
<SC.CollapseToggler />
</SC.FirstLabel>
}
>
{item.children.map(node => plantTree(node))}
</Tree>
</SC.TreeWrapper>
) )
} }
@@ -91,7 +124,10 @@ export function ResourcesSidebar() {
return ( return (
<SC.ResourcesSidebarWrapper> <SC.ResourcesSidebarWrapper>
{tree.map(node => plantTree(node))} <SC.Banner src={banner} />
<SC.Inner>
{tree.map((node, index) => plantTree(node, index, true))}
</SC.Inner>
</SC.ResourcesSidebarWrapper> </SC.ResourcesSidebarWrapper>
) )
} }
+102 -1
View File
@@ -1,10 +1,111 @@
import styled from "styled-components" import styled from "styled-components"
import { Link } from "gatsby" import { Link } from "gatsby"
import ChevronUp from "../../icons/chevron-up.svg"
export const ResourcesSidebarWrapper = styled.div` export const ResourcesSidebarWrapper = styled.div`
box-sizing: border-box; box-sizing: border-box;
padding: 0 16px;
flex: 0 0 300px; flex: 0 0 300px;
background: #f9f9f9;
`
export const Banner = styled.img`
display: block;
width: 100%;
`
export const TreeWrapper = styled.div`
width: 100%;
& + & {
border-top: 1px solid #dbdbdb;
}
`
export const Label = styled.div`
padding: 4px 0;
`
export const FirstLabel = styled.div`
display: flex;
align-items: center;
width: 100%;
padding: 12px 15px 12px 0;
font-weight: 700;
color: #a9a9a9;
&.active {
color: #000;
}
svg {
margin-left: auto;
transform: rotate(90deg);
transition: transform 0.3s;
path {
fill: #a9a9a9;
}
}
&.active svg {
transform: rotate(0);
path {
fill: #000;
}
}
`
export const CollapseToggler = styled(ChevronUp)``
export const Inner = styled.div`
padding-top: 30px;
padding-left: 20px;
.tree-view {
overflow-y: hidden;
}
.tree-view_item {
display: flex;
cursor: pointer;
}
.tree-view_children {
margin-left: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.tree-view_children-collapsed {
height: 0px;
}
.tree-view_arrow {
cursor: pointer;
margin-right: 6px;
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tree-view_arrow:after {
content: "▾";
}
.tree-view_arrow.firstLevel {
display: none;
}
.tree-view_arrow-collapsed {
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
transform: rotate(-90deg);
}
` `
export const PageLink = styled(Link)` export const PageLink = styled(Link)`
@@ -5,6 +5,6 @@ created_at: 2019/07/27
title: Callbacks title: Callbacks
--- ---
# Callbacks are everywhere ## Callbacks are everywhere
If you've been doing javascript for any amount of time you'll have noticed that callbacks appear in just about every single piece of code. If you've been doing javascript for any amount of time you'll have noticed that callbacks appear in just about every single piece of code.
@@ -2,10 +2,9 @@
authors: authors:
- "veksen#1060" - "veksen#1060"
created_at: "2019/07/27" created_at: "2019/07/27"
title: Iterative vs Functional array helpers
--- ---
# Iterative vs Functional array helpers
-- placeholder -- -- placeholder --
## find ## find
@@ -9,14 +9,14 @@ recommended_reading:
- javascript/es6/arrow-functions - javascript/es6/arrow-functions
--- ---
# A Promise to Keep ## A Promise to Keep
A `Promise` in Javascript represents an action that has already started, but one that will be A `Promise` in Javascript represents an action that has already started, but one that will be
completed at a later time. Much like in real life, when you create a promise, you are expected completed at a later time. Much like in real life, when you create a promise, you are expected
to fulfill that promise. However, sometimes things go wrong where you can no longer fulfill to fulfill that promise. However, sometimes things go wrong where you can no longer fulfill
a promise you made. This is essentially the main idea behind how promises work in javascript. a promise you made. This is essentially the main idea behind how promises work in javascript.
## Basics ### Basics
When you create a Promise or call a function that returns a Promise in Javascript, you're left When you create a Promise or call a function that returns a Promise in Javascript, you're left
with an object that can either resolve into the actual value that you were promised, or it with an object that can either resolve into the actual value that you were promised, or it
@@ -24,7 +24,7 @@ can reject and leave you with an error for why that promise failed.
We can access these values using the `.then` and `.catch` methods on the `Promise` object respectively. We can access these values using the `.then` and `.catch` methods on the `Promise` object respectively.
## A Simple Example ### A Simple Example
First, let's explore a bit of a made-up example. Imagine we have a promise-returning function First, let's explore a bit of a made-up example. Imagine we have a promise-returning function
called `getMembers` that retrieves all the members in a discord server. When we execute this called `getMembers` that retrieves all the members in a discord server. When we execute this
@@ -50,7 +50,7 @@ getMembers("The Programmers Hangout").then(members => {
This way we are able to make sure that we only try to `console.log` when the `getMembers` function has resolved and ready to be used. This way we are able to make sure that we only try to `console.log` when the `getMembers` function has resolved and ready to be used.
## Beginner Mistakes ### Beginner Mistakes
Promises are possibly the #1 most common source of confusion for beginners. In order Promises are possibly the #1 most common source of confusion for beginners. In order
to avoid falling in pitfalls yourself, you have to remember 2 things about Javascript when to avoid falling in pitfalls yourself, you have to remember 2 things about Javascript when
@@ -85,7 +85,7 @@ getWeather("Los Angeles").then(weather => {
}) })
``` ```
## Real World Example ### Real World Example
Here is a real function that gets character information from the Rick and Morty API. Here is a real function that gets character information from the Rick and Morty API.
You can try it in your browser if you want to test it out. You can try it in your browser if you want to test it out.
@@ -2,10 +2,9 @@
authors: authors:
- "Xetera#0001" - "Xetera#0001"
created_at: "2019/07/26" created_at: "2019/07/26"
title: Simplifying Promises
--- ---
# Simplifying Promises
-- placeholder -- -- placeholder --
The first naive attempt, using new Promise for something that already returns a promise. The first naive attempt, using new Promise for something that already returns a promise.
@@ -2,10 +2,9 @@
authors: authors:
- "supergrecko#3434" - "supergrecko#3434"
created_at: "2019/07/27" created_at: "2019/07/27"
title: Singletons
--- ---
# Singletons
A singleton is a class which is only instantiated once during runtime. This is done by keeping a static property containing its instance on the singleton class. A singleton is a class which is only instantiated once during runtime. This is done by keeping a static property containing its instance on the singleton class.
There are multiple benefits to using a singleton class There are multiple benefits to using a singleton class
@@ -16,7 +15,7 @@ There are multiple benefits to using a singleton class
By using a singleton instead of a static class we expose a cleaner class to use and we can use regular instance properties instead of static properties. By using a singleton instead of a static class we expose a cleaner class to use and we can use regular instance properties instead of static properties.
# Creating a Singleton in PHP ## Creating a Singleton in PHP
Creating a class which can be used as a singleton is very simple. Creating a class which can be used as a singleton is very simple.
@@ -50,7 +49,7 @@ class Singleton
} }
``` ```
# Testing our Singleton ## Testing our Singleton
To give a little functionality to our freshly baked Singleton we add these three members to the class To give a little functionality to our freshly baked Singleton we add these three members to the class
@@ -93,7 +92,7 @@ $first->setWord("Banana");
var_dump($first->getWord() === $second->getWord()); // bool(true) var_dump($first->getWord() === $second->getWord()); // bool(true)
``` ```
# Further Research ## Further Research
Here's a couple links which will help you understand Singletons better. Here's a couple links which will help you understand Singletons better.
+2 -1
View File
@@ -2,9 +2,10 @@
authors: authors:
- "Xetera#0001" - "Xetera#0001"
date: 2019/06/26 date: 2019/06/26
title: Some Python Article
--- ---
# Python ## Python
This is a placeholder post This is a placeholder post
+18
View File
@@ -5,6 +5,24 @@ export const BASE_FONT_SIZE = 18
export const BASE_LINE_HEIGHT = 25 export const BASE_LINE_HEIGHT = 25
export const MODULAR_SCALE = 1.1487 export const MODULAR_SCALE = 1.1487
function modularFontSize(power: number) {
return BASE_FONT_SIZE * Math.pow(MODULAR_SCALE, power)
}
export function modularScale(power: number) {
const fontSize = modularFontSize(power)
// attempt to fit line-height a little larger than font-size
const paddedLineHeight = fontSize * 1.1
const lineHeight =
paddedLineHeight - (paddedLineHeight % BASE_LINE_HEIGHT) + BASE_LINE_HEIGHT
return {
fontSize,
lineHeight,
}
}
export const fontFamily = { export const fontFamily = {
header: FONT_MONTSERRAT, header: FONT_MONTSERRAT,
body: FONT_OXYGEN, body: FONT_OXYGEN,
+1 -47
View File
@@ -11,6 +11,7 @@ export const GlobalStyles = createGlobalStyle`
} }
body { body {
margin: 0;
font-size: ${BASE_FONT_SIZE}px; font-size: ${BASE_FONT_SIZE}px;
line-height: ${BASE_LINE_HEIGHT}px; line-height: ${BASE_LINE_HEIGHT}px;
} }
@@ -33,51 +34,4 @@ export const GlobalStyles = createGlobalStyle`
padding-left: 2.8em; padding-left: 2.8em;
overflow: initial; overflow: initial;
} }
/* TODO: probably extract this */
.tree-view {
overflow-y: hidden;
}
.tree-view_item {
display: flex;
cursor: pointer;
}
.tree-view_item,
.tree-view_children > div {
padding: 4px 0;
}
.tree-view_children {
margin-left: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.tree-view_children-collapsed {
height: 0px;
}
.tree-view_arrow {
cursor: pointer;
margin-right: 6px;
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tree-view_arrow:after {
content: "▾";
}
.tree-view_arrow-collapsed {
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
transform: rotate(-90deg);
}
` `
+12
View File
@@ -0,0 +1,12 @@
import { useContext } from "react"
import { SidebarContext, SidebarContextInterface } from "../SidebarProvider"
export default function useSidebar(): SidebarContextInterface {
const sidebarContext = useContext(SidebarContext)
if (!sidebarContext) {
throw Error("Need context")
}
return sidebarContext
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="15" height="8" viewBox="0 0 15 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.72578 0.21176L0.213835 6.72587C-0.0712782 7.01171 -0.0712782 7.47481 0.213835 7.76136C0.498947 8.0472 0.962045 8.0472 1.24716 7.76136L7.24241 1.76394L13.2377 7.76064C13.5228 8.04648 13.9859 8.04648 14.2717 7.76064C14.5568 7.47481 14.5568 7.01099 14.2717 6.72515L7.75976 0.211038C7.47759 -0.0704669 7.00722 -0.070466 6.72578 0.21176Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+12 -2
View File
@@ -3,14 +3,22 @@ import { graphql } from "gatsby"
import { Markdown } from "../components/Markdown" import { Markdown } from "../components/Markdown"
import { SEO } from "../components/SEO" import { SEO } from "../components/SEO"
import { ResourcesLayout } from "../components/ResourcesLayout" import { ResourcesLayout } from "../components/ResourcesLayout"
import { ResourceHeader } from "../components/ResourceHeader"
// @todo maybe find alternative type for data // @todo maybe find alternative type for data
function LanguagePost({ data }: any) { function LanguagePost({ data }: any) {
const { html, frontmatter } = data.file.post const { html, fields, frontmatter, timeToRead } = data.file.post
console.log(data) console.log(data)
return ( return (
<ResourcesLayout> <ResourcesLayout>
<SEO title={frontmatter.title} /> <SEO title={frontmatter.title} />
<ResourceHeader
title={frontmatter.title}
authors={fields.authors}
createdAt={frontmatter.created_at}
timeToRead={timeToRead}
recommendedReading={frontmatter.recommended_reading}
/>
<Markdown content={html} /> <Markdown content={html} />
</ResourcesLayout> </ResourcesLayout>
) )
@@ -31,9 +39,11 @@ export const query = graphql`
} }
} }
frontmatter { frontmatter {
date created_at
title title
recommended_reading
} }
timeToRead
} }
} }
} }