diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..535dfcc1 --- /dev/null +++ b/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": ["next/babel"], + "plugins": [ + "transform-flow-strip-types", + [ + "styled-components", + { + "ssr": true, + "displayName": true, + "preprocess": false + } + ] + ] +} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..993caf76 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,126 @@ +{ + "extends": [ + "airbnb", + "prettier", + "plugin:react/recommended" + ], + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "impliedStrict": true, + "classes": true + } + }, + "env": { + "browser": true, + "node": true, + "jquery": true, + "jest": true + }, + "rules": { + "no-debugger": 0, + "no-alert": 0, + "no-unused-vars": [ + 1, + { + "ignoreSiblings": true, + "argsIgnorePattern": "res|next|^err" + } + ], + "prefer-const": [ + "error", + { + "destructuring": "all" + } + ], + "arrow-body-style": [ + 2, + "as-needed" + ], + "no-unused-expressions": [ + 2, + { + "allowTaggedTemplates": true + } + ], + "no-param-reassign": [ + 2, + { + "props": false + } + ], + "no-console": 0, + "import/prefer-default-export": 0, + "import": 0, + "func-names": 0, + "space-before-function-paren": 0, + "comma-dangle": 0, + "max-len": 0, + "import/extensions": 0, + "no-underscore-dangle": 0, + "consistent-return": 0, + "react/display-name": 1, + "react/no-array-index-key": 0, + "react/react-in-jsx-scope": 0, + "react/prefer-stateless-function": 0, + "react/forbid-prop-types": 0, + "react/no-unescaped-entities": 0, + "jsx-a11y/accessible-emoji": 0, + "react/jsx-one-expression-per-line": 0, + "react/require-default-props": 0, + "react/jsx-filename-extension": [ + 1, + { + "extensions": [ + ".js", + ".jsx" + ] + } + ], + "radix": 0, + "no-shadow": [ + 2, + { + "hoist": "all", + "allow": [ + "resolve", + "reject", + "done", + "next", + "err", + "error" + ] + } + ], + "quotes": [ + 2, + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": true + } + ], + "prettier/prettier": [ + "error", + { + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80 + } + ], + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/anchor-is-valid": [ + "warn", + { + "aspects": [ + "invalidHref" + ] + } + ] + }, + "plugins": [ + "prettier" + ] +} \ No newline at end of file diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000..b401683f --- /dev/null +++ b/.flowconfig @@ -0,0 +1,26 @@ +[ignore] +/node_modules + +[include] + +[libs] +./flow-typed + +[options] +suppress_comment=.*\\$FlowFixMe +suppress_comment=.*\\$FlowIssue +esproposal.class_instance_fields=enable +module.system.node.resolve_dirname=node_modules +module.system.node.resolve_dirname=. +module.file_ext=.js +module.file_ext=.jsx +module.file_ext=.json + +[lints] +untyped-type-import=error +untyped-import=warn +unclear-type=warn +unsafe-getters-setters=error + +[version] +0.90.0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad46b308..12bdd4f5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +.DS_Store # Runtime data pids diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..ed38be3c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "prettier.printWidth": 120, + "prettier.tabWidth": 2, + "prettier.singleQuote": true, + "prettier.trailingComma": "none", + "prettier.bracketSpacing": true, + "prettier.parser": "flow", + "prettier.semi": true, + "prettier.useTabs": false, + "prettier.jsxBracketSameLine": false +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..81a70634 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: node_js +node_js: + - 10 +cache: + directories: + - ~/.npm + - ~/.cache +install: + - npm install +before_script: + - npm run build + - npm run start & +script: + - node --version + - npm --version + - npm run flow + - npm run cypress:run +notifications: + email: false \ No newline at end of file diff --git a/components/Button/Button.js b/components/Button/Button.js new file mode 100644 index 00000000..c4280f0a --- /dev/null +++ b/components/Button/Button.js @@ -0,0 +1,9 @@ +// @flow +import React from 'react'; +import * as Styled from './style'; +import type { ButtonProps } from './types'; + +export default function Button(props: ButtonProps) { + const { children } = props; + return {children}; +} diff --git a/components/Button/CopyLinkButton.js b/components/Button/CopyLinkButton.js new file mode 100644 index 00000000..63bdc5cd --- /dev/null +++ b/components/Button/CopyLinkButton.js @@ -0,0 +1,45 @@ +// @flow +// $FlowIssue +import React, { useState } from 'react'; +import dynamic from 'next/dynamic'; +import * as Styled from './style'; +import Icon from '../Icon'; +import type { ButtonProps } from './types'; + +const Clipboard = dynamic(() => import('react-clipboard.js'), { + ssr: false, + loading: () => null, +}); + +type CopyLinkProps = { + ...$Exact, + text: string, +}; + +export default function CopyLinkButton(props: CopyLinkProps) { + const { text, children } = props; + const [isClicked, handleClick] = useState(false); + + const onClick = () => { + handleClick(true); + setTimeout(() => handleClick(false), 2000); + }; + + return ( + + + + {isClicked ? 'Copied!' : children} + + + ); +} diff --git a/components/Button/FacebookButton.js b/components/Button/FacebookButton.js new file mode 100644 index 00000000..467c1cb9 --- /dev/null +++ b/components/Button/FacebookButton.js @@ -0,0 +1,15 @@ +// @flow +import React from 'react'; +import * as Styled from './style'; +import Icon from '../Icon'; +import type { ButtonProps } from './types'; + +export default function FacebookButton(props: ButtonProps) { + const { children } = props; + return ( + + + {children} + + ); +} diff --git a/components/Button/GhostButton.js b/components/Button/GhostButton.js new file mode 100644 index 00000000..08ebe271 --- /dev/null +++ b/components/Button/GhostButton.js @@ -0,0 +1,9 @@ +// @flow +import React from 'react'; +import * as Styled from './style'; +import type { ButtonProps } from './types'; + +export default function GhostButton(props: ButtonProps) { + const { children } = props; + return {children}; +} diff --git a/components/Button/OutlineButton.js b/components/Button/OutlineButton.js new file mode 100644 index 00000000..d8f9b009 --- /dev/null +++ b/components/Button/OutlineButton.js @@ -0,0 +1,9 @@ +// @flow +import React from 'react'; +import * as Styled from './style'; +import type { ButtonProps } from './types'; + +export default function OutlineButton(props: ButtonProps) { + const { children } = props; + return {children}; +} diff --git a/components/Button/PrimaryButton.js b/components/Button/PrimaryButton.js new file mode 100644 index 00000000..a86d39c6 --- /dev/null +++ b/components/Button/PrimaryButton.js @@ -0,0 +1,9 @@ +// @flow +import React from 'react'; +import * as Styled from './style'; +import type { ButtonProps } from './types'; + +export default function PrimaryButton(props: ButtonProps) { + const { children } = props; + return {children}; +} diff --git a/components/Button/TwitterButton.js b/components/Button/TwitterButton.js new file mode 100644 index 00000000..a225a38d --- /dev/null +++ b/components/Button/TwitterButton.js @@ -0,0 +1,15 @@ +// @flow +import React from 'react'; +import * as Styled from './style'; +import Icon from '../Icon'; +import type { ButtonProps } from './types'; + +export default function TwitterButton(props: ButtonProps) { + const { children } = props; + return ( + + + {children} + + ); +} diff --git a/components/Button/index.js b/components/Button/index.js new file mode 100644 index 00000000..cb86e785 --- /dev/null +++ b/components/Button/index.js @@ -0,0 +1,22 @@ +// @flow +import * as Styled from './style'; +import Button from './Button'; +import CopyLinkButton from './CopyLinkButton'; +import FacebookButton from './FacebookButton'; +import GhostButton from './GhostButton'; +import OutlineButton from './OutlineButton'; +import PrimaryButton from './PrimaryButton'; +import TwitterButton from './TwitterButton'; + +const { ButtonRow } = Styled; + +export { + Button, + CopyLinkButton, + FacebookButton, + GhostButton, + OutlineButton, + PrimaryButton, + TwitterButton, + ButtonRow, +}; diff --git a/components/Button/style.js b/components/Button/style.js new file mode 100644 index 00000000..37dbe8a7 --- /dev/null +++ b/components/Button/style.js @@ -0,0 +1,363 @@ +// @flow +import styled, { css } from 'styled-components'; +import { hexa, tint } from '../globals'; +import type { ButtonSize } from './types'; +import { theme } from '../theme'; + +const getPadding = (size: ButtonSize) => { + switch (size) { + case 'small': + return '4px 8px'; + case 'default': + return '10px 20px'; + case 'large': + return '14px 28px'; + default: { + return '10px 20px'; + } + } +}; + +const getFontSize = (size: ButtonSize) => { + switch (size) { + case 'small': + return '14px'; + case 'default': + return '16px'; + case 'large': + return '18px'; + default: { + return '16px'; + } + } +}; + +const base = css` + -webkit-appearance: none; + display: flex; + flex: none; + align-self: center; + align-items: center; + justify-content: center; + border-radius: 4px; + font-size: ${props => getFontSize(props.size)}; + font-weight: 500; + white-space: nowrap; + word-break: keep-all; + transition: all 0.2s ease-in-out; + cursor: pointer; + line-height: 1; + position: relative; + text-align: center; + padding: ${props => getPadding(props.size)}; + opacity: ${props => (props.disabled ? '0.64' : '1')}; + box-shadow: ${props => + props.disabled ? 'none' : `0 1px 2px rgba(0,0,0,0.04)`}; + + &:disabled { + cursor: not-allowed; + } + + &:hover { + transition: all 0.2s ease-in-out; + box-shadow: ${props => + props.disabled ? 'none' : `${theme.shadows.button}`}; + } +`; + +export const Button = styled.button` + ${base} + border: 1px solid ${theme.border.default}; + color: ${theme.text.secondary}; + background-color: ${theme.bg.default}; + background-image: ${props => + `linear-gradient(to bottom, ${props.theme.bg.default}, ${ + props.theme.bg.wash + })`}; + + &:hover { + color: ${theme.text.default}; + } + + &:active { + border: 1px solid ${theme.border.active}; + background-image: ${props => + `linear-gradient(to top, ${props.theme.bg.default}, ${ + props.theme.bg.wash + })`}; + } + + &:focus { + box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${ + theme.border.default +}; + } +`; + +export const PrimaryButton = styled.button` + ${base} + border: 1px solid ${theme.brand.default}; + color: ${theme.bg.default}; + background-color: ${theme.brand.alt}; + background-image: ${props => + `linear-gradient(to bottom, ${props.theme.brand.alt}, ${ + props.theme.brand.default + })`}; + text-shadow: 0 1px 1px rgba(0,0,0,0.08); + + &:hover { + color: ${theme.text.reverse}; + background-image: ${props => + `linear-gradient(to bottom, ${tint(props.theme.brand.alt, 16)}, ${tint( + props.theme.brand.default, + 16 + )})`}; + box-shadow: ${props => (props.disabled ? 'none' : theme.shadows.button)}; + } + + &:active { + border: 1px solid ${theme.brand.default}; + background-image: ${props => + `linear-gradient(to top, ${props.theme.brand.alt}, ${ + props.theme.brand.default + })`}; + } + + &:focus { + box-shadow: 0 0 0 1px ${props => + props.theme.bg.default}, 0 0 0 3px ${props => + hexa(props.theme.brand.alt, 0.16)}; + } +`; + +export const GhostButton = styled.button` + ${base} border: none; + color: ${theme.text.secondary}; + box-shadow: none; + background-color: transparent; + background-image: none; + + &:hover { + background: ${props => tint(props.theme.bg.wash, -3)}; + color: ${theme.text.default}; + box-shadow: none; + } + + &:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.08)}; + } +`; + +export const OutlineButton = styled.button` + ${base} + border: 1px solid ${theme.border.default}; + color: ${theme.text.secondary}; + background-color: transparent; + background-image: none; + + &:hover { + color: ${theme.text.default}; + border: 1px solid ${theme.border.active}; + box-shadow: none; + } + + &:active { + border: 1px solid ${theme.text.placeholder}; + } + + &:focus { + box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${ + theme.border.default +}; + } +`; + +export const ButtonRow = styled.div` + display: flex; + align-items: center; + + @media (max-width: 968px) { + flex-wrap: nowrap; + + button { + margin-top: 8px; + } + } +`; + +export const ButtonSegmentRow = styled.div` + display: flex; + align-items: center; + justify-content: center; + position: relative; + + button { + z-index: 1; + } + + button:active, + button:focus { + z-index: 2; + } + + button:first-of-type:not(:last-of-type) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + button:last-of-type:not(:first-of-type) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + button:not(:last-of-type):not(:first-of-type) { + border-radius: 0; + position: relative; + margin: 0 -1px; + } + + ${PrimaryButton} { + &:focus { + box-shadow: 0 0 0 1px ${theme.bg.default}, + 0 0 0 3px ${props => hexa(props.theme.brand.alt, 0.16)}; + } + } +`; + +export const FacebookButton = styled.button` + ${base} + border: 1px solid ${theme.social.facebook}; + color: ${theme.bg.default}; + background-color: ${theme.social.facebook}; + background-image: ${props => + `linear-gradient(to bottom, ${props.theme.social.facebook}, ${ + props.theme.social.facebook + })`}; + text-shadow: 0 1px 1px rgba(0,0,0,0.08); + + .icon { + margin-right: 8px; + margin-left: -4px; + } + + &:hover { + color: ${theme.text.reverse}; + background-image: ${props => + `linear-gradient(to bottom, ${tint( + props.theme.social.facebook, + 16 + )}, ${tint(props.theme.social.facebook, 16)})`}; + box-shadow: ${props => (props.disabled ? 'none' : theme.shadows.button)}; + } + + &:active { + border: 1px solid ${theme.social.facebook}; + background-image: ${props => + `linear-gradient(to top, ${props.theme.social.facebook}, ${ + props.theme.social.facebook + })`}; + } + + &:focus { + box-shadow: 0 0 0 1px ${props => + props.theme.bg.default}, 0 0 0 3px ${props => + hexa(props.theme.social.facebook, 0.16)}; + } +`; + +export const TwitterButton = styled.button` + ${base} + border: 1px solid ${theme.social.twitter}; + color: ${theme.bg.default}; + background-color: ${theme.social.twitter}; + background-image: ${props => + `linear-gradient(to bottom, ${props.theme.social.twitter}, ${ + props.theme.social.twitter + })`}; + text-shadow: 0 1px 1px rgba(0,0,0,0.08); + + .icon { + margin-right: 8px; + margin-left: -4px; + } + + &:hover { + color: ${theme.text.reverse}; + background-image: ${props => + `linear-gradient(to bottom, ${tint( + props.theme.social.twitter, + 4 + )}, ${tint(props.theme.social.twitter, 4)})`}; + box-shadow: ${props => (props.disabled ? 'none' : theme.shadows.button)}; + } + + &:active { + border: 1px solid ${theme.social.twitter}; + background-image: ${props => + `linear-gradient(to top, ${props.theme.social.twitter}, ${ + props.theme.social.twitter + })`}; + } + + &:focus { + box-shadow: 0 0 0 1px ${props => + props.theme.bg.default}, 0 0 0 3px ${props => + hexa(props.theme.social.twitter, 0.16)}; + } +`; + +export const CopyLinkButton = styled.button` + ${base} + border: 1px solid ${props => + props.isClicked + ? tint(props.theme.success.default, -10) + : props.theme.border.default}; + color: ${props => + props.isClicked ? props.theme.bg.default : props.theme.text.secondary}; + background-color: ${props => + props.isClicked ? props.theme.success.default : props.theme.bg.default}; + background-image: ${props => + `linear-gradient(to bottom, ${ + props.isClicked ? props.theme.success.default : props.theme.bg.default + }, ${ + props.isClicked + ? tint(props.theme.success.default, -4) + : props.theme.bg.wash + })`}; + transition: border 0.2s ease-in-out, background-color 0.2s ease-in-out, background-image 0.2s ease-in-out; + + &:hover { + transition: border 0.2s ease-in-out, background-color 0.2s ease-in-out, background-image 0.2s ease-in-out; + color: ${props => + props.isClicked ? props.theme.bg.default : props.theme.text.default}; + } + + &:active { + border: 1px solid ${props => + props.isClicked + ? tint(props.theme.success.default, -10) + : props.theme.border.active}; + background-image: ${props => + `linear-gradient(to bottom, ${ + props.isClicked + ? tint(props.theme.success.default, -4) + : props.theme.bg.default + }, ${ + props.isClicked ? props.theme.success.default : props.theme.bg.wash + })`}; + } + + .icon { + margin-right: 8px; + margin-left: -4px; + } + + &:focus { + box-shadow: 0 0 0 1px ${props => + props.theme.bg.default}, 0 0 0 3px ${props => + props.isClicked + ? hexa(props.theme.success.default, 0.16) + : props.theme.border.default}; + } +`; diff --git a/components/Button/types.js b/components/Button/types.js new file mode 100644 index 00000000..f151d6cf --- /dev/null +++ b/components/Button/types.js @@ -0,0 +1,9 @@ +// @flow +import type { Node } from 'react'; + +export type ButtonSize = 'small' | 'large' | 'default'; +export type ButtonProps = { + size?: ButtonSize, + disabled?: boolean, + children: Node | string, +}; diff --git a/components/Card/index.js b/components/Card/index.js new file mode 100644 index 00000000..0be2ca70 --- /dev/null +++ b/components/Card/index.js @@ -0,0 +1,13 @@ +// @flow +import * as React from 'react'; +import { StyledCard } from './style'; + +type Props = { + children: React.Node, + style?: Object, +}; + +export default function Card(props: Props) { + const { style, children } = props; + return {children}; +} diff --git a/components/Card/style.js b/components/Card/style.js new file mode 100644 index 00000000..109117da --- /dev/null +++ b/components/Card/style.js @@ -0,0 +1,18 @@ +// @flow +import styled from 'styled-components'; +import { Shadows } from '../globals'; + +export const StyledCard = styled.div` + position: relative; + background: ${props => props.theme.bg.default}; + border-radius: 6px; + ${Shadows.default}; + + &:hover { + ${Shadows.hover}; + } + + &:active { + ${Shadows.active}; + } +`; diff --git a/components/Footer/index.js b/components/Footer/index.js new file mode 100644 index 00000000..cb56b325 --- /dev/null +++ b/components/Footer/index.js @@ -0,0 +1,32 @@ +// @flow +import React from 'react'; +import { Container, Description, Icons } from './style'; +import Icon from '../Icon'; + +export default function Footer() { + return ( + + + + + + + + + Copyright whenever. This is + + open source + + . + + + ); +} diff --git a/components/Footer/style.js b/components/Footer/style.js new file mode 100644 index 00000000..ea3a1e5c --- /dev/null +++ b/components/Footer/style.js @@ -0,0 +1,44 @@ +// @flow +import styled from 'styled-components'; +import { theme } from '../theme'; + +export const Container = styled.div` + margin-top: 128px; + padding: 0 16px; + width: 100%; +`; + +export const Description = styled.p` + font-size: 14px; + color: ${theme.text.tertiary}; + max-width: 320px; + display: flex; + flex: 1 0 auto; + align-items: flex-start; + padding-bottom: 16px; + + a { + color: ${theme.text.default}; + margin-left: 4px; + } +`; + +export const Icons = styled.div` + display: flex; + flex: 1 0 auto; + align-items: flex-start; + margin-left: -16px; + padding-bottom: 8px; + + a { + color: ${theme.text.tertiary}; + } + + a:hover { + color: ${theme.text.default}; + } + + .icon { + margin-left: 16px; + } +`; diff --git a/components/Header/index.js b/components/Header/index.js new file mode 100644 index 00000000..55b108f4 --- /dev/null +++ b/components/Header/index.js @@ -0,0 +1,39 @@ +// @flow +import * as React from 'react'; +import Link from 'next/link'; +import { Container, Logo, ButtonRowContainer } from './style'; +import { PrimaryButton, GhostButton } from '../Button'; + +type Props = { + showHeaderShadow: boolean, +}; + +export default function Header(props: Props) { + const { showHeaderShadow } = props; + + return ( + + + + Security Checklist + + + + + + + About + + + + + Contribute + + + + ); +} diff --git a/components/Header/style.js b/components/Header/style.js new file mode 100644 index 00000000..c781060d --- /dev/null +++ b/components/Header/style.js @@ -0,0 +1,43 @@ +// @flow +import styled from 'styled-components'; +import { theme } from '../theme'; + +export const Container = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: 'logo actions'; + padding: 16px; + position: fixed; + top: 0; + left: 0; + right: 0; + background: ${props => + props.showHeaderShadow ? props.theme.bg.default : props.theme.bg.wash}; + z-index: 3; + box-shadow: ${props => + props.showHeaderShadow ? '0 4px 8px rgba(0,0,0,0.04)' : 'none'}; + transition: all 0.2s ease-in-out; + + @media (max-width: 968px) { + padding: 8px 16px; + grid-template-columns: 1fr 1fr; + grid-template-areas: 'logo actions'; + } +`; + +export const Logo = styled.h1` + grid-area: logo; + font-size: 18px; + font-weight: 700; + color: ${theme.text.default}; +`; + +export const ButtonRowContainer = styled.div` + display: flex; + justify-content: flex-end; + grid-area: actions; + align-items: center; + + @media (max-width: 968px) { + } +`; diff --git a/components/Icon/index.js b/components/Icon/index.js new file mode 100644 index 00000000..13254765 --- /dev/null +++ b/components/Icon/index.js @@ -0,0 +1,101 @@ +// @flow +import * as React from 'react'; +import styled from 'styled-components'; + +type Props = { + glyph: string, + size?: number, +}; + +export const InlineSvg = styled.svg` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; + width: 100%; + color: inherit; + fill: currentColor; +`; + +export const SvgWrapper = styled.div` + display: inline-block; + flex: 0 0 ${props => (props.size ? `${props.size}px` : '32px')}; + width: ${props => (props.size ? `${props.size}px` : '32px')}; + height: ${props => (props.size ? `${props.size}px` : '32px')}; + min-width: ${props => (props.size ? `${props.size}px` : '32px')}; + min-height: ${props => (props.size ? `${props.size}px` : '32px')}; + position: relative; + color: inherit; +`; + +type GlyphProps = { + glyph: string, +}; + +export const Glyph = ({ glyph }: GlyphProps): any => { + switch (glyph) { + case 'facebook': + return ( + + + + ); + case 'link': + return ( + + + + ); + case 'share': + return ( + + + + ); + case 'twitter': + return ( + + + + ); + case 'view-forward': + return ( + + + + ); + case 'github': + return ( + + + + ); + default: + return null; + } +}; + +export default function Icon(props: Props) { + const { size = 32, glyph } = props; + + return ( + + + {glyph} + + + + ); +} diff --git a/components/Page/index.js b/components/Page/index.js new file mode 100644 index 00000000..83612f18 --- /dev/null +++ b/components/Page/index.js @@ -0,0 +1,85 @@ +// @flow +// $FlowIssue +import React, { useState, useEffect } from 'react'; +import type { Node } from 'react'; +import { ThemeProvider } from 'styled-components'; +import { throttle } from 'throttle-debounce'; +import Icon from '../Icon'; +import Header from '../Header'; +import Footer from '../Footer'; +import { theme } from '../theme'; +import { + Container, + SectionHeading, + Heading, + Subheading, + LargeHeading, + LargeSubheading, + InnerContainer, + ScrollToTop, +} from './style'; +import * as gtag from '../../lib/gtag'; + +export { SectionHeading, Heading, Subheading, LargeHeading, LargeSubheading }; + +type Props = { + children: Node, +}; + +export default function Page(props: Props) { + const { children } = props; + const [lastTrackedPageview, setLastTrackedPageview] = useState(null); + const [showHeaderShadow, setHeaderShadow] = useState(false); + const [scrollToTopVisible, setScrollToTopVisible] = useState(false); + + function handleScroll() { + const headerShadowState = window && window.pageYOffset > 0; + const scrollToTopState = window && window.pageYOffset > 240; + setHeaderShadow(headerShadowState); + setScrollToTopVisible(scrollToTopState); + } + + const throttledScroll = throttle(300, handleScroll); + + const scrollToTop = () => { + if (window) { + window.scrollTo(0, 0); + } + }; + + useEffect(() => { + if (window) { + window.addEventListener('scroll', throttledScroll); + } + + return () => { + if (window) { + window.removeEventListener('scroll', throttledScroll); + setLastTrackedPageview(null); + } + }; + }, []); + + useEffect(() => { + if (document) { + const newLocation = document.location.pathname; + if (newLocation !== lastTrackedPageview) { + gtag.pageview(document.location.pathname); + setLastTrackedPageview(newLocation); + } + } + }); + + return ( + + +
+ {children} +