├── .babelrc ├── .eslintrc.json ├── .flowconfig ├── .github ├── release-drafter.yml └── workflows │ └── main.yml ├── .gitignore ├── .nowignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── components ├── BottomShare │ ├── index.js │ └── style.js ├── Button │ ├── Button.js │ ├── CopyLinkButton.js │ ├── FacebookButton.js │ ├── GhostButton.js │ ├── OutlineButton.js │ ├── PrimaryButton.js │ ├── TwitterButton.js │ ├── index.js │ ├── style.js │ └── types.js ├── Card │ ├── index.js │ └── style.js ├── Checklist │ ├── index.js │ └── style.js ├── ChecklistItem │ ├── App.js │ ├── Apps.js │ ├── Heading.js │ ├── Offer.js │ ├── Resource.js │ ├── Resources.js │ ├── index.js │ ├── style.js │ └── utils.js ├── Footer │ ├── index.js │ └── style.js ├── Header │ ├── Confetti.js │ ├── Logo.js │ ├── index.js │ └── style.js ├── Icon │ └── index.js ├── LoadingChecklistItem │ ├── index.js │ └── style.js ├── Page │ ├── index.js │ └── style.js ├── Providers │ └── index.js ├── ShareButtons │ ├── index.js │ └── style.js ├── globals │ └── index.js └── theme │ └── index.js ├── config ├── appPermissions.js ├── browsers.js ├── carrierPin.js ├── creditFreeze.js ├── data.js ├── dns.js ├── email.js ├── encryptedHardware.js ├── geotagging.js ├── messagingApps.js ├── next-seo.js ├── passwordManager.js ├── patching.js ├── phishing.js ├── physicalPrivacy.js ├── searchEngine.js ├── socialMedia.js ├── strongDevicePasscode.js ├── twoFactor.js └── vpn.js ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── about_spec.js │ └── home_spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── flow-typed └── npm │ ├── @sentry │ └── browser_vx.x.x.js │ ├── next-seo_vx.x.x.js │ ├── next_vx.x.x.js │ ├── react-clipboard.js_vx.x.x.js │ ├── react-markdown_vx.x.x.js │ ├── styled-components_vx.x.x.js │ └── throttle-debounce_vx.x.x.js ├── lib └── localStorage.js ├── next.config.js ├── now.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── _error.js ├── about.js └── index.js ├── static ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── img │ ├── 1111.jpg │ ├── 1password.jpg │ ├── 9999.jpg │ ├── authy.jpg │ ├── avast-passwords.png │ ├── bitwarden.jpg │ ├── brave.jpg │ ├── burnermail.jpg │ ├── buttercup.png │ ├── cliqz.png │ ├── dashlane.jpg │ ├── duckduckgo.jpg │ ├── encrypt-me.jpg │ ├── expressvpn.jpg │ ├── fastmail.jpg │ ├── firefox.jpg │ ├── google-authenticator.jpg │ ├── guardian.png │ ├── icloud.png │ ├── imessage.png │ ├── ivpn.jpg │ ├── jumbo.png │ ├── lastpass.jpg │ ├── mailfence.png │ ├── microsoft_authenticator_80.png │ ├── nordvpn.jpg │ ├── privateinternetaccess.jpg │ ├── protonmail.jpg │ ├── protonvpn.jpg │ ├── purevpn.jpg │ ├── safari.jpg │ ├── signal.jpg │ ├── simplelogin.png │ ├── tor.png │ ├── tutanota.jpg │ ├── vivaldi.png │ ├── whatsapp.jpg │ └── yubico.jpg ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── ms-icon-70x70.png ├── normalize.js ├── og-image.png └── pinned-tab.svg ├── types └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | "@babel/plugin-transform-flow-strip-types", 5 | [ 6 | "styled-components", 7 | { 8 | "ssr": true, 9 | "displayName": true, 10 | "preprocess": false 11 | } 12 | ] 13 | ] 14 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier/@typescript-eslint", 7 | "plugin:prettier/recommended" 8 | ], 9 | "plugins": ["react", "@typescript-eslint", "prettier"], 10 | "env": { 11 | "browser": true, 12 | "jasmine": true, 13 | "jest": true 14 | }, 15 | "rules": { 16 | "prettier/prettier": ["error", { "singleQuote": true }], 17 | "react/display-name": "off", 18 | "react/react-in-jsx-scope": "off", 19 | "@typescript-eslint/explicit-function-return-type": "off", 20 | "@typescript-eslint/camelcase": "off", 21 | "@typescript-eslint/ban-ts-ignore": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | }, 24 | "settings": { 25 | "react": { 26 | "pragma": "React", 27 | "version": "detect" 28 | } 29 | }, 30 | "parser": "@typescript-eslint/parser" 31 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules 3 | 4 | [include] 5 | 6 | [libs] 7 | ./flow-typed 8 | 9 | [options] 10 | suppress_comment=.*\\$FlowFixMe 11 | suppress_comment=.*\\$FlowIssue 12 | esproposal.class_instance_fields=enable 13 | module.system.node.resolve_dirname=node_modules 14 | module.system.node.allow_root_relative=true 15 | module.file_ext=.js 16 | module.file_ext=.jsx 17 | module.file_ext=.json 18 | 19 | [lints] 20 | untyped-type-import=error 21 | untyped-import=warn 22 | unclear-type=warn 23 | unsafe-getters-setters=error 24 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## Changes since last release 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: E2E on Chrome 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | cypress-run: 11 | runs-on: ubuntu-latest 12 | # let's make sure our tests pass on Chrome browser 13 | name: E2E on Chrome 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Cypress run 18 | uses: cypress-io/github-action@v2 19 | with: 20 | build: yarn run build 21 | start: yarn run start 22 | browser: chrome 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .DS_Store 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | -------------------------------------------------------------------------------- /.nowignore: -------------------------------------------------------------------------------- 1 | cypress 2 | README.md 3 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.printWidth": 120, 3 | "prettier.tabWidth": 2, 4 | "prettier.singleQuote": true, 5 | "prettier.trailingComma": "none", 6 | "prettier.bracketSpacing": true, 7 | "prettier.parser": "flow", 8 | "prettier.semi": true, 9 | "prettier.useTabs": false, 10 | "prettier.jsxBracketSameLine": false 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brian Lovin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Security checklist 2 | A checklist for staying safe on the internet. 3 | 4 | ### This project has moved 5 | In the interest of maintaining fewer services, codebases, and domains, I've integrated this project into my [personal website](https://github.com/brianlovin/brian-lovin-next). View the live project at [brianlovin.com/security](https://brianlovin.com/security). 6 | 7 | ### Motivation 8 | This project is the result of a conversation started during a [recent episode](https://spec.fm/podcasts/design-details/249464) of the [Design Details Podcast](https://spec.fm/podcasts/design-details/) and a subsequent tweet by [Michael Knepprath](https://twitter.com/mknepprath/status/1083966912420372481). 9 | 10 | ### Contributing 11 | This project should be considered a living document of resources and applications that improve people's digital security and privacy. Contributions, edits, and extensions are welcome! 12 | 13 | If you have resources to add to existing sections, please open a pull request. 14 | 15 | - Aim for reputable sources for guides and news coverage. 16 | - If adding an app, please include links to as many platforms as possible. See `config/*.js` for examples of how data is formatted. 17 | - Try to use approachable human-readable language. Remember that even non-technical folks need to stay safe online. 18 | 19 | If you would like to create a new category of security and privacy resources, please open an issue first with your proposed category. Please explain why this additional category should stand alone from other existing sections. 20 | 21 | ### Run this locally 22 | 1. `$ git clone git@github.com:brianlovin/security-checklist.git` 23 | 2. `$ cd security-checklist` 24 | 3. `$ npm install` 25 | 4. `$ npm run dev` 26 | 5. View the running app in your browser at `http://localhost:3000` 27 | 28 | ### Deploying 29 | You can deploy this project yourself with ZEIT + Now by configuring `now.json` and running `$ now`. 30 | 31 | ### Feedback 32 | Please open issues at any time for general feedback, or you can reach me directly at hi@brianlovin.com. 33 | -------------------------------------------------------------------------------- /components/BottomShare/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import Card from '../Card'; 4 | import ShareButtons from '../ShareButtons'; 5 | import { CardContent, TopBorder } from './style'; 6 | import { SmallHeading, SmallSubheading } from '../Page/style'; 7 | 8 | const BottomShare = () => ( 9 | 10 | 11 | 12 | Feeling more secure? 13 | 14 | Spread the word about improving online privacy and security 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default BottomShare; 22 | -------------------------------------------------------------------------------- /components/BottomShare/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | export const CardContent = styled.div` 5 | padding: 24px; 6 | width: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | flex-direction: column; 11 | text-align: center; 12 | 13 | @media (max-width: 768px) { 14 | padding-top: 40px; 15 | } 16 | `; 17 | 18 | export const TopBorder = styled.div` 19 | width: 100%; 20 | height: 4px; 21 | background-image: linear-gradient(to left, #a913de, #6ac9ff); 22 | border-radius: 6px 6px 0 0; 23 | `; 24 | -------------------------------------------------------------------------------- /components/Button/Button.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Styled from './style'; 4 | import type { ButtonProps } from './types'; 5 | 6 | export default function Button(props: ButtonProps) { 7 | const { children } = props; 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components/Button/CopyLinkButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowIssue 3 | import React, { useState } from 'react'; 4 | import { CopyLinkButton as StyledCopyLinkButton } from './style'; 5 | import Icon from '../Icon'; 6 | import type { ButtonProps } from './types'; 7 | 8 | type CopyLinkProps = { 9 | ...$Exact, 10 | text: string, 11 | }; 12 | 13 | export default function CopyLinkButton(props: CopyLinkProps) { 14 | const { text, children } = props; 15 | const [isClicked, handleClick] = useState(false); 16 | 17 | const onClick = () => { 18 | handleClick(true); 19 | setTimeout(() => handleClick(false), 2000); 20 | }; 21 | 22 | return ( 23 | 34 | 35 | {isClicked ? 'Copied!' : children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/Button/FacebookButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Styled from './style'; 4 | import Icon from '../Icon'; 5 | import type { ButtonProps } from './types'; 6 | 7 | export default function FacebookButton(props: ButtonProps) { 8 | const { children } = props; 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/Button/GhostButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Styled from './style'; 4 | import type { ButtonProps } from './types'; 5 | 6 | export default function GhostButton(props: ButtonProps) { 7 | const { children } = props; 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components/Button/OutlineButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Styled from './style'; 4 | import type { ButtonProps } from './types'; 5 | 6 | export default function OutlineButton(props: ButtonProps) { 7 | const { children } = props; 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components/Button/PrimaryButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Styled from './style'; 4 | import type { ButtonProps } from './types'; 5 | 6 | export default function PrimaryButton(props: ButtonProps) { 7 | const { children } = props; 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components/Button/TwitterButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Styled from './style'; 4 | import Icon from '../Icon'; 5 | import type { ButtonProps } from './types'; 6 | 7 | export default function TwitterButton(props: ButtonProps) { 8 | const { children } = props; 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/Button/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Styled from './style'; 3 | import Button from './Button'; 4 | import CopyLinkButton from './CopyLinkButton'; 5 | import FacebookButton from './FacebookButton'; 6 | import GhostButton from './GhostButton'; 7 | import OutlineButton from './OutlineButton'; 8 | import PrimaryButton from './PrimaryButton'; 9 | import TwitterButton from './TwitterButton'; 10 | 11 | const { ButtonRow } = Styled; 12 | 13 | export { 14 | Button, 15 | CopyLinkButton, 16 | FacebookButton, 17 | GhostButton, 18 | OutlineButton, 19 | PrimaryButton, 20 | TwitterButton, 21 | ButtonRow, 22 | }; 23 | -------------------------------------------------------------------------------- /components/Button/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled, { css } from 'styled-components'; 3 | import { hexa, tint } from '../globals'; 4 | import type { ButtonSize } from './types'; 5 | import { theme } from '../theme'; 6 | import dynamic from 'next/dynamic'; 7 | 8 | const Clipboard = dynamic(() => import('react-clipboard.js'), { 9 | ssr: false, 10 | loading: () => null, 11 | }); 12 | 13 | const getPadding = (size: ButtonSize) => { 14 | switch (size) { 15 | case 'small': 16 | return '4px 8px'; 17 | case 'default': 18 | return '10px 20px'; 19 | case 'large': 20 | return '14px 28px'; 21 | default: { 22 | return '10px 20px'; 23 | } 24 | } 25 | }; 26 | 27 | const getFontSize = (size: ButtonSize) => { 28 | switch (size) { 29 | case 'small': 30 | return '14px'; 31 | case 'default': 32 | return '16px'; 33 | case 'large': 34 | return '18px'; 35 | default: { 36 | return '16px'; 37 | } 38 | } 39 | }; 40 | 41 | export const base = css` 42 | -webkit-appearance: none; 43 | display: flex; 44 | flex: none; 45 | align-self: center; 46 | align-items: center; 47 | justify-content: center; 48 | border-radius: 4px; 49 | font-size: ${props => getFontSize(props.size)}; 50 | font-weight: 500; 51 | white-space: nowrap; 52 | word-break: keep-all; 53 | transition: all 0.2s ease-in-out; 54 | cursor: pointer; 55 | line-height: 1; 56 | position: relative; 57 | text-align: center; 58 | padding: ${props => getPadding(props.size)}; 59 | opacity: ${props => (props.disabled ? '0.64' : '1')}; 60 | box-shadow: ${props => 61 | props.disabled ? 'none' : `0 1px 2px rgba(0,0,0,0.04)`}; 62 | 63 | &:disabled { 64 | cursor: not-allowed; 65 | } 66 | 67 | &:hover, &:active, &:focus { 68 | transition: all 0.2s ease-in-out; 69 | box-shadow: ${props => 70 | props.disabled ? 'none' : `${theme.shadows.button}`}; 71 | } 72 | &:active, &:focus { 73 | box-shadow: 0 0 0 1px ${theme.bg.default}, 74 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.5)}; 75 | } 76 | `; 77 | 78 | export const Button = styled.button` 79 | ${base} 80 | border: 1px solid ${theme.border.default}; 81 | color: ${theme.text.secondary}; 82 | background-color: ${theme.bg.default}; 83 | background-image: ${props => 84 | `linear-gradient(to bottom, ${props.theme.bg.default}, ${ 85 | props.theme.bg.wash 86 | })`}; 87 | 88 | &:hover { 89 | color: ${theme.text.default}; 90 | } 91 | 92 | &:active { 93 | border: 1px solid ${theme.border.active}; 94 | background-image: ${props => 95 | `linear-gradient(to top, ${props.theme.bg.default}, ${ 96 | props.theme.bg.wash 97 | })`}; 98 | } 99 | 100 | &:focus { 101 | box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${ 102 | theme.border.default 103 | }; 104 | } 105 | `; 106 | 107 | export const PrimaryButton = styled.button` 108 | ${base} 109 | border: 1px solid ${theme.brand.default}; 110 | color: ${theme.bg.default}; 111 | background-color: ${theme.brand.alt}; 112 | background-image: ${props => 113 | `linear-gradient(to bottom, ${props.theme.brand.alt}, ${ 114 | props.theme.brand.default 115 | })`}; 116 | text-shadow: 0 1px 1px rgba(0,0,0,0.08); 117 | 118 | &:hover { 119 | color: ${theme.text.reverse}; 120 | background-image: ${props => 121 | `linear-gradient(to bottom, ${tint(props.theme.brand.alt, 16)}, ${tint( 122 | props.theme.brand.default, 123 | 16 124 | )})`}; 125 | box-shadow: ${props => (props.disabled ? 'none' : theme.shadows.button)}; 126 | } 127 | 128 | &:active { 129 | border: 1px solid ${theme.brand.default}; 130 | background-image: ${props => 131 | `linear-gradient(to top, ${props.theme.brand.alt}, ${ 132 | props.theme.brand.default 133 | })`}; 134 | } 135 | 136 | &:focus { 137 | box-shadow: 0 0 0 1px ${props => 138 | props.theme.bg.default}, 0 0 0 3px ${props => 139 | hexa(props.theme.brand.alt, 0.5)}; 140 | } 141 | `; 142 | 143 | export const GhostButton = styled.button` 144 | ${base} border: none; 145 | color: ${theme.text.secondary}; 146 | box-shadow: none; 147 | background-color: transparent; 148 | background-image: none; 149 | 150 | &:hover, &:active, &:focus { 151 | background: ${props => tint(props.theme.bg.wash, -8)}; 152 | color: ${theme.text.default}; 153 | box-shadow: none; 154 | } 155 | 156 | &:active, &:focus { 157 | box-shadow: 0 0 0 1px ${theme.bg.default}, 158 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 159 | } 160 | `; 161 | 162 | export const OutlineButton = styled.button` 163 | ${base} 164 | border: 1px solid ${theme.border.default}; 165 | color: ${theme.text.secondary}; 166 | background-color: transparent; 167 | background-image: none; 168 | 169 | &:hover { 170 | color: ${theme.text.default}; 171 | border: 1px solid ${theme.border.active}; 172 | box-shadow: none; 173 | } 174 | 175 | &:active { 176 | border: 1px solid ${theme.text.placeholder}; 177 | } 178 | 179 | &:focus { 180 | box-shadow: 0 0 0 1px ${props => props.theme.bg.default}, 0 0 0 3px ${ 181 | theme.border.default 182 | }; 183 | } 184 | `; 185 | 186 | export const ButtonRow = styled.div` 187 | display: flex; 188 | align-items: center; 189 | 190 | @media (max-width: 968px) { 191 | flex-wrap: nowrap; 192 | 193 | button { 194 | margin-top: 8px; 195 | } 196 | } 197 | `; 198 | 199 | export const ButtonSegmentRow = styled.div` 200 | display: flex; 201 | align-items: center; 202 | justify-content: center; 203 | position: relative; 204 | 205 | button { 206 | z-index: 1; 207 | } 208 | 209 | button:active, 210 | button:focus { 211 | z-index: 2; 212 | } 213 | 214 | button:first-of-type:not(:last-of-type) { 215 | border-top-right-radius: 0; 216 | border-bottom-right-radius: 0; 217 | } 218 | 219 | button:last-of-type:not(:first-of-type) { 220 | border-top-left-radius: 0; 221 | border-bottom-left-radius: 0; 222 | } 223 | 224 | button:not(:last-of-type):not(:first-of-type) { 225 | border-radius: 0; 226 | position: relative; 227 | margin: 0 -1px; 228 | } 229 | 230 | ${PrimaryButton} { 231 | &:focus { 232 | box-shadow: 0 0 0 1px ${theme.bg.default}, 233 | 0 0 0 3px ${props => hexa(props.theme.brand.alt, 0.5)}; 234 | } 235 | } 236 | `; 237 | 238 | export const FacebookButton = styled.a` 239 | ${base} 240 | border: 1px solid ${theme.social.facebook}; 241 | color: ${theme.bg.default}; 242 | background-color: ${theme.social.facebook}; 243 | background-image: ${props => 244 | `linear-gradient(to bottom, ${props.theme.social.facebook}, ${ 245 | props.theme.social.facebook 246 | })`}; 247 | text-shadow: 0 1px 1px rgba(0,0,0,0.08); 248 | 249 | .icon { 250 | margin-right: 8px; 251 | margin-left: -4px; 252 | } 253 | 254 | &:hover { 255 | color: ${theme.text.reverse}; 256 | background-image: ${props => 257 | `linear-gradient(to bottom, ${tint( 258 | props.theme.social.facebook, 259 | 16 260 | )}, ${tint(props.theme.social.facebook, 16)})`}; 261 | box-shadow: ${props => (props.disabled ? 'none' : theme.shadows.button)}; 262 | } 263 | 264 | &:active { 265 | border: 1px solid ${theme.social.facebook}; 266 | background-image: ${props => 267 | `linear-gradient(to top, ${props.theme.social.facebook}, ${ 268 | props.theme.social.facebook 269 | })`}; 270 | } 271 | 272 | &:focus { 273 | box-shadow: 0 0 0 1px ${props => 274 | props.theme.bg.default}, 0 0 0 3px ${props => 275 | hexa(props.theme.social.facebook, 0.5)}; 276 | } 277 | `; 278 | 279 | export const TwitterButton = styled.a` 280 | ${base} 281 | border: 1px solid ${theme.social.twitter}; 282 | color: ${theme.bg.default}; 283 | background-color: ${theme.social.twitter}; 284 | background-image: ${props => 285 | `linear-gradient(to bottom, ${props.theme.social.twitter}, ${ 286 | props.theme.social.twitter 287 | })`}; 288 | text-shadow: 0 1px 1px rgba(0,0,0,0.08); 289 | 290 | .icon { 291 | margin-right: 8px; 292 | margin-left: -4px; 293 | } 294 | 295 | &:hover { 296 | color: ${theme.text.reverse}; 297 | background-image: ${props => 298 | `linear-gradient(to bottom, ${tint( 299 | props.theme.social.twitter, 300 | 4 301 | )}, ${tint(props.theme.social.twitter, 4)})`}; 302 | box-shadow: ${props => (props.disabled ? 'none' : theme.shadows.button)}; 303 | } 304 | 305 | &:active { 306 | border: 1px solid ${theme.social.twitter}; 307 | background-image: ${props => 308 | `linear-gradient(to top, ${props.theme.social.twitter}, ${ 309 | props.theme.social.twitter 310 | })`}; 311 | } 312 | 313 | &:focus { 314 | box-shadow: 0 0 0 1px ${props => 315 | props.theme.bg.default}, 0 0 0 3px ${props => 316 | hexa(props.theme.social.twitter, 0.5)}; 317 | } 318 | `; 319 | 320 | export const CopyLinkButton = styled(Clipboard)` 321 | ${base} 322 | transition: all ${theme.animations.default}; 323 | border: 1px solid ${props => 324 | props.isClicked 325 | ? tint(props.theme.success.default, -10) 326 | : props.theme.border.default}; 327 | color: ${props => 328 | props.isClicked ? props.theme.bg.default : props.theme.text.secondary}; 329 | background-color: ${props => 330 | props.isClicked ? props.theme.success.default : props.theme.bg.default}; 331 | background-image: ${props => 332 | `linear-gradient(to bottom, ${ 333 | props.isClicked ? props.theme.success.default : props.theme.bg.default 334 | }, ${ 335 | props.isClicked 336 | ? tint(props.theme.success.default, -4) 337 | : props.theme.bg.wash 338 | })`}; 339 | transition: border 0.2s ease-in-out, background-color 0.2s ease-in-out, background-image 0.2s ease-in-out; 340 | 341 | &:hover { 342 | transition: border 0.2s ease-in-out, background-color 0.2s ease-in-out, background-image 0.2s ease-in-out; 343 | color: ${props => 344 | props.isClicked ? props.theme.bg.default : props.theme.text.default}; 345 | } 346 | 347 | &:active { 348 | border: 1px solid ${props => 349 | props.isClicked 350 | ? tint(props.theme.success.default, -10) 351 | : props.theme.border.active}; 352 | background-image: ${props => 353 | `linear-gradient(to bottom, ${ 354 | props.isClicked 355 | ? tint(props.theme.success.default, -4) 356 | : props.theme.bg.default 357 | }, ${ 358 | props.isClicked ? props.theme.success.default : props.theme.bg.wash 359 | })`}; 360 | } 361 | 362 | .icon { 363 | margin-right: 8px; 364 | margin-left: -4px; 365 | } 366 | 367 | &:focus { 368 | box-shadow: 0 0 0 1px ${props => 369 | props.theme.bg.default}, 0 0 0 3px ${props => 370 | props.isClicked 371 | ? hexa(props.theme.success.default, 0.5) 372 | : props.theme.border.default}; 373 | } 374 | `; 375 | 376 | -------------------------------------------------------------------------------- /components/Button/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Node } from 'react'; 3 | 4 | export type ButtonSize = 'small' | 'large' | 'default'; 5 | export type ButtonProps = { 6 | size?: ButtonSize, 7 | disabled?: boolean, 8 | children: Node | string, 9 | }; 10 | -------------------------------------------------------------------------------- /components/Card/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { StyledCard } from './style'; 4 | 5 | type Props = { 6 | children: React.Node, 7 | style?: Object, 8 | }; 9 | 10 | export default function Card(props: Props) { 11 | const { style, children, ...rest } = props; 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Card/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | import { Shadows } from '../globals'; 4 | 5 | export const StyledCard = styled.div` 6 | position: relative; 7 | background: ${props => props.theme.bg.default}; 8 | border-radius: 6px; 9 | ${Shadows.default}; 10 | `; 11 | -------------------------------------------------------------------------------- /components/Checklist/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import data from '../../config/data'; 4 | import ChecklistItem from '../ChecklistItem'; 5 | import { Grid } from './style'; 6 | 7 | class Checklist extends React.Component<{}> { 8 | render() { 9 | const keys = Object.keys(data); 10 | const resources = keys.map(k => data[k]); 11 | return ( 12 | 13 | {resources.map(resource => ( 14 | 15 | ))} 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default Checklist; 22 | -------------------------------------------------------------------------------- /components/Checklist/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | export const Grid = styled.div` 5 | display: grid; 6 | grid-template-columns: 1fr; 7 | margin-top: 48px; 8 | width: 100%; 9 | `; 10 | -------------------------------------------------------------------------------- /components/ChecklistItem/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { App } from '../../types'; 4 | import { 5 | AppMeta, 6 | AppRowContainer, 7 | AppIcon, 8 | AppName, 9 | AppSourcesList, 10 | AppSourcesListItem, 11 | AppSourcesLabel, 12 | } from './style'; 13 | import Offer from './Offer'; 14 | import Icon from '../Icon'; 15 | 16 | type Props = { 17 | app: App, 18 | }; 19 | 20 | export const AppRow = ({ app }: Props) => { 21 | const sourcesKeys = app.sources && Object.keys(app.sources); 22 | 23 | const renderSourceIcon = (key: string) => { 24 | const sourceUrl = app.sources[key]; 25 | const renderMatch = key.toLowerCase(); 26 | const hideOnMobile = 27 | renderMatch === 'linux' || 28 | renderMatch === 'macos' || 29 | renderMatch === 'windows'; 30 | return ( 31 | 32 | 33 | 34 | {renderMatch} 35 | 36 | 37 | ); 38 | }; 39 | 40 | return ( 41 | 42 | 43 | {app.image && } 44 | {app.name} 45 | 46 | {sourcesKeys && ( 47 | {sourcesKeys.map(renderSourceIcon)} 48 | )} 49 | {app.offer && } 50 | 51 | ); 52 | }; -------------------------------------------------------------------------------- /components/ChecklistItem/Apps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowIssue 3 | import React, { useState, useRef } from 'react'; 4 | import { AppsContainer, SectionHeading, ExpandContainer, ExpandContent } from './style'; 5 | import { Button } from '../Button'; 6 | import type { ChecklistResource } from '../../types'; 7 | import { AppRow } from './App'; 8 | 9 | type Props = { 10 | resource: ChecklistResource, 11 | handleAppsExpand: Function, 12 | }; 13 | 14 | export const Apps = ({ resource, handleAppsExpand }: Props) => { 15 | const [overflowExpanded, setOverflowExpanded] = useState(false); 16 | const [contentHeight, setcontentHeight] = useState(2000); 17 | 18 | const expandContentContainer = useRef(null); 19 | 20 | function handleExpand() { 21 | let expandContentHeight = 22 | expandContentContainer.current 23 | ? expandContentContainer.current.scrollHeight 24 | : contentHeight; 25 | 26 | setcontentHeight(expandContentHeight) 27 | setOverflowExpanded(!overflowExpanded) 28 | 29 | handleAppsExpand(overflowExpanded ? -expandContentHeight : expandContentHeight); 30 | } 31 | 32 | if (!resource.apps) return null; 33 | 34 | let appList = resource.apps; 35 | let overflowAppList; 36 | if (appList && appList.length > 3) { 37 | overflowAppList = appList.slice(3, appList.length); 38 | appList = appList.slice(0, 3); 39 | } 40 | 41 | return ( 42 | 43 | Apps 44 | {appList && appList.map(app => ( 45 | 46 | ))} 47 | 48 | {overflowAppList && ( 49 | 50 | 58 | {overflowAppList.map(app => )} 59 | 60 | 61 | 67 | 68 | 69 | 70 | )} 71 | 72 | ); 73 | }; 74 | 75 | -------------------------------------------------------------------------------- /components/ChecklistItem/Heading.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Title, Uncollapse } from './style'; 4 | import type { ChecklistResource } from '../../types'; 5 | 6 | type Props = { 7 | resource: ChecklistResource, 8 | isCollapsed: boolean, 9 | handleCollapse: Function, 10 | }; 11 | 12 | export const Heading = ({ resource, isCollapsed, handleCollapse }: Props) => ( 13 | 14 | 15 | {resource.title} 16 | <Uncollapse 17 | onClick={handleCollapse} 18 | aria-controls={`content_${resource.id}`} 19 | aria-expanded={!isCollapsed} 20 | type="button" 21 | > 22 | {isCollapsed ? 'Show details' : 'Hide details'} 23 | </Uncollapse> 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /components/ChecklistItem/Offer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { Offer } from '../../types'; 4 | import Icon from '../Icon'; 5 | import { OfferContainer, LeftBorder } from './style'; 6 | 7 | type Props = { 8 | offer: Offer, 9 | }; 10 | 11 | export default function AppOffer({ offer }: Props) { 12 | return ( 13 | 14 | 15 | 16 | {offer.label} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/ChecklistItem/Resource.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { Resource } from '../../types'; 4 | import { ResourceRowContainer, ResourceName } from './style'; 5 | import Icon from '../Icon'; 6 | 7 | type Props = { 8 | resource: Resource, 9 | }; 10 | 11 | export const ResourceRow = ({ resource }: Props) => ( 12 | 17 | 18 | {resource.name} 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /components/ChecklistItem/Resources.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { SectionHeading } from './style'; 4 | import type { ChecklistResource } from '../../types'; 5 | import { ResourceRow } from './Resource'; 6 | 7 | type Props = { 8 | resource: ChecklistResource, 9 | }; 10 | 11 | export const Resources = ({ resource }: Props) => { 12 | if (!resource.resources) return null; 13 | return ( 14 | 15 | Resources 16 | {resource.resources.map(r => ( 17 | 18 | ))} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /components/ChecklistItem/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { ChecklistResource } from '../../types'; 4 | import { getCheckedStatusById, setCheckedStatusById } from './utils'; 5 | import Card from '../Card'; 6 | import LoadingChecklistItem from '../LoadingChecklistItem'; 7 | import { Heading } from './Heading'; 8 | import { Apps } from './Apps'; 9 | import { Resources } from './Resources'; 10 | import { 11 | Container, 12 | CheckboxContainer, 13 | CardContent, 14 | ResourceContent, 15 | Divider, 16 | Description, 17 | Content, 18 | } from './style'; 19 | 20 | type Props = { 21 | resource: ChecklistResource, 22 | }; 23 | 24 | type State = { 25 | isChecked: boolean, 26 | isLoading: boolean, 27 | isCollapsed: boolean, 28 | contentHeight: number, 29 | }; 30 | 31 | class ChecklistItem extends React.Component { 32 | state = { isChecked: false, isLoading: true, isCollapsed: true, contentHeight: 2000, }; 33 | contentContainer: { current: null | HTMLDivElement } 34 | 35 | constructor(props: Props) { 36 | super(props); 37 | 38 | this.contentContainer = React.createRef(); 39 | } 40 | 41 | componentDidMount() { 42 | const { resource } = this.props; 43 | const isChecked = getCheckedStatusById(resource.id); 44 | return this.setState({ 45 | isChecked, 46 | isLoading: false, 47 | isCollapsed: isChecked, 48 | }); 49 | } 50 | 51 | componentDidUpdate(prevProps: Props, prevState: State) { 52 | if (prevState.isLoading && !this.state.isLoading && this.contentContainer.current) { 53 | return this.setState({ 54 | contentHeight: this.contentContainer.current.scrollHeight, 55 | }) 56 | } 57 | } 58 | 59 | handleSetChecked = () => { 60 | const { isChecked } = this.state; 61 | const { resource } = this.props; 62 | setCheckedStatusById(resource.id, !isChecked); 63 | return this.setState({ isChecked: !isChecked, isCollapsed: !isChecked }); 64 | }; 65 | 66 | uncollapse = () => { 67 | this.setState(state => ({ isCollapsed: !state.isCollapsed })); 68 | this.contentContainer.current && this.contentContainer.current.focus(); 69 | }; 70 | 71 | handleAppsExpand = (appsContainerHeight: number) => { 72 | return this.contentContainer.current && this.setState({ 73 | contentHeight: this.contentContainer.current.scrollHeight + appsContainerHeight, 74 | }) 75 | } 76 | 77 | render() { 78 | const { isChecked, isLoading, isCollapsed, contentHeight } = this.state; 79 | const { resource } = this.props; 80 | 81 | if (isLoading) return ; 82 | 83 | return ( 84 | 85 | 86 | 87 | 88 | 96 | 99 | 100 | 101 | 105 | 110 | 111 | 119 | 120 | 121 | 122 | {resource.apps && ( 123 | 124 | 125 | 129 | 130 | )} 131 | 132 | {resource.resources && ( 133 | 134 | 135 | 136 | 137 | )} 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | ); 146 | } 147 | } 148 | 149 | export default ChecklistItem; 150 | -------------------------------------------------------------------------------- /components/ChecklistItem/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled, { css } from 'styled-components'; 3 | import Markdown from 'react-markdown'; 4 | import { theme } from '../theme'; 5 | import { Shadows, tint, hexa } from '../globals'; 6 | 7 | export const Container = styled.div` 8 | margin-bottom: 24px; 9 | `; 10 | 11 | export const CardContent = styled.div` 12 | padding: 24px 16px; 13 | display: flex; 14 | width: 100%; 15 | 16 | @media (max-width: 768px) { 17 | flex-direction: ${props => (props.isCollapsed ? 'row' : 'column')}; 18 | } 19 | `; 20 | 21 | export const Title = styled.h3` 22 | font-size: 18px; 23 | font-weight: 500; 24 | color: ${theme.text.default}; 25 | display: flex; 26 | line-height: 1.4; 27 | justify-content: space-between; 28 | align-items: flex-start; 29 | `; 30 | 31 | export const Description = styled(Markdown)` 32 | font-size: 16px; 33 | font-weight: 400; 34 | color: ${theme.text.secondary}; 35 | margin-top: 8px; 36 | padding-right: 16px; 37 | 38 | p:first-of-type { 39 | margin-top: 0; 40 | } 41 | 42 | p { 43 | margin-top: 12px; 44 | } 45 | `; 46 | 47 | export const SectionHeading = styled.h5` 48 | font-size: 12px; 49 | font-weight: 500; 50 | color: ${theme.text.tertiary}; 51 | text-transform: uppercase; 52 | letter-spacing: 0.5px; 53 | 54 | @media (max-width: 768px) { 55 | margin-top: 24px; 56 | margin-bottom: 8px; 57 | } 58 | `; 59 | 60 | export const CheckboxContainer = styled.div` 61 | display: flex; 62 | align-items: flex-start; 63 | width: 48px; 64 | justify-content: center; 65 | 66 | @media (max-width: 768px) { 67 | width: auto; 68 | margin-left: 8px; 69 | margin-top: 4px; 70 | width: 32px; 71 | } 72 | 73 | input[type='checkbox'] { 74 | position: absolute; 75 | } 76 | 77 | input[type='checkbox'] + label { 78 | width: 32px; 79 | height: 32px; 80 | border-radius: 4px; 81 | border: 1px solid ${theme.border.default}; 82 | background: ${theme.bg.wash}; 83 | cursor: pointer; 84 | position: relative; 85 | overflow: hidden; 86 | text-indent: -1000px; 87 | } 88 | 89 | input[type='checkbox'] + label:hover { 90 | ${Shadows.default}; 91 | background: ${theme.bg.default}; 92 | } 93 | 94 | input[type='checkbox'] + label::after { 95 | content: ''; 96 | position: absolute; 97 | display: block; 98 | left: 11px; 99 | top: 6px; 100 | width: 6px; 101 | height: 12px; 102 | border: solid ${theme.border.active}; 103 | border-width: 0 2px 2px 0; 104 | transform: rotate(45deg); 105 | } 106 | 107 | input[type='checkbox']:checked + label { 108 | border: 1px solid ${theme.bg.default}; 109 | } 110 | 111 | input[type='checkbox']:checked + label::after { 112 | border: solid ${theme.bg.default}; 113 | border-width: 0 2px 2px 0; 114 | } 115 | 116 | /* This ::before pseudo-element is used to animate the gradient 117 | which does not support transitions. */ 118 | input[type='checkbox'] + label::before { 119 | content: ''; 120 | position: absolute; 121 | top: 0; 122 | left: 0; 123 | right: 0; 124 | bottom: 0; 125 | transition: opacity ${theme.animations.default}; 126 | opacity: 0; 127 | background-image: radial-gradient(circle at top right, #a913de, #6ac9ff); 128 | box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.4); 129 | } 130 | 131 | input[type='checkbox']:checked + label::before { 132 | opacity: 1; 133 | background-color: ${theme.success.default}; 134 | } 135 | 136 | input[type='checkbox']:active + label, 137 | input[type='checkbox']:focus + label { 138 | box-shadow: 0 0 0 1px ${theme.bg.default}, 139 | 0 0 0 3px ${props => hexa(props.theme.brand.default, 0.5)}; 140 | } 141 | input[type='checkbox']:active:checked + label, 142 | input[type='checkbox']:focus:checked + label { 143 | box-shadow: 0 0 0 1px ${theme.bg.default}, 144 | 0 0 0 3px ${props => hexa(props.theme.spectrum.default, 0.5)}; 145 | } 146 | `; 147 | 148 | export const ResourceContent = styled.div` 149 | display: flex; 150 | flex-direction: column; 151 | margin-top: 2px; 152 | padding-left: 16px; 153 | width: 100%; 154 | justify-content: ${props => (props.isCollapsed ? 'center' : 'flex-start')}; 155 | 156 | @media (max-width: 768px) { 157 | padding-left: ${props => (props.isCollapsed ? '16px' : '8px')}; 158 | margin-top: ${props => (props.isCollapsed ? '0px' : '24px')}; 159 | justify-content: ${props => (props.isCollapsed ? 'center' : 'flex-start')}; 160 | } 161 | `; 162 | 163 | export const AppsContainer = styled.div``; 164 | 165 | export const AppRowContainer = styled.div` 166 | display: flex; 167 | justify-content: space-between; 168 | align-items: center; 169 | margin-top: 8px; 170 | width: 100%; 171 | border-radius: 6px; 172 | padding: 4px 8px; 173 | padding-left: 16px; 174 | margin-left: -16px; 175 | padding-right: 4px; 176 | width: calc(100% + 12px); 177 | flex-wrap: wrap; 178 | 179 | @media (max-width: 768px) { 180 | border-top: 1px solid ${theme.bg.wash}; 181 | border-radius: 0; 182 | margin-top: 0px; 183 | margin-left: -24px; 184 | width: calc(100% + 40px); 185 | padding: 12px 24px; 186 | 187 | &:last-of-type { 188 | border-bottom: 1px solid ${theme.bg.wash}; 189 | } 190 | 191 | &:hover { 192 | background: ${theme.bg.default}!important; 193 | } 194 | } 195 | 196 | &:hover { 197 | background: ${theme.bg.wash}; 198 | } 199 | `; 200 | 201 | export const AppMeta = styled.a` 202 | display: flex; 203 | align-items: center; 204 | padding-right: 6px; 205 | border-radius: 4px; 206 | 207 | &:active, 208 | &:focus { 209 | box-shadow: 0 0 0 1px ${theme.bg.default}, 210 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 211 | } 212 | `; 213 | 214 | export const AppIcon = styled.img` 215 | width: 40px; 216 | height: 40px; 217 | border-radius: 8px; 218 | margin-right: 12px; 219 | 220 | @media (max-width: 768px) { 221 | width: 32px; 222 | height: 32px; 223 | border-radius: 6px; 224 | } 225 | `; 226 | 227 | export const AppName = styled.p` 228 | font-size: 16px; 229 | font-weight: 400; 230 | color: ${theme.text.secondary}; 231 | `; 232 | 233 | export const AppSourcesList = styled.ul` 234 | list-style-type: none; 235 | display: flex; 236 | align-items: center; 237 | margin-right: 8px; 238 | 239 | @media (max-width: 768px) { 240 | margin-right: 0; 241 | justify-content: flex-start; 242 | } 243 | `; 244 | 245 | export const AppSourcesListItem = styled.li` 246 | display: flex; 247 | flex-direction: column; 248 | align-items: center; 249 | justify-content: center; 250 | color: ${theme.text.tertiary}; 251 | padding: 2px 8px; 252 | min-width: 56px; 253 | transition: all 0.1s ease-in-out; 254 | 255 | a { 256 | display: flex; 257 | flex-direction: column; 258 | align-items: center; 259 | justify-content: center; 260 | padding: 2px; 261 | border-radius: 4px; 262 | 263 | &:hover { 264 | color: ${theme.text.default}; 265 | } 266 | 267 | &:active, 268 | &:focus { 269 | color: ${theme.text.default}; 270 | box-shadow: 0 0 0 1px ${theme.bg.default}, 271 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 272 | } 273 | } 274 | 275 | .icon { 276 | position: relative; 277 | top: 5px; 278 | left: 4px; 279 | } 280 | 281 | @media (max-width: 768px) { 282 | padding: 4px; 283 | min-width: 40px; 284 | 285 | .icon { 286 | position: relative; 287 | top: 3px; 288 | left: 5px; 289 | } 290 | 291 | &:hover { 292 | background: ${theme.bg.default}!important; 293 | } 294 | 295 | ${props => 296 | props.hideOnMobile && 297 | css` 298 | display: none; 299 | `} 300 | } 301 | `; 302 | 303 | export const AppSourcesLabel = styled.span` 304 | font-size: 12px; 305 | font-weight: 400; 306 | margin-top: 4px; 307 | 308 | @media (max-width: 768px) { 309 | display: none; 310 | } 311 | `; 312 | 313 | export const ResourceRowContainer = styled.a` 314 | display: flex; 315 | margin-top: 4px; 316 | width: 100%; 317 | border-radius: 6px; 318 | padding: 8px 20px 8px 12px; 319 | margin-left: -12px; 320 | color: ${theme.text.tertiary}; 321 | width: calc(100% + 8px); 322 | line-height: 1.3; 323 | 324 | &:first-of-type { 325 | margin-top: 8px; 326 | } 327 | 328 | .icon { 329 | margin-right: 8px; 330 | } 331 | 332 | &:hover, 333 | &:active, 334 | &:focus { 335 | color: ${theme.text.default}; 336 | } 337 | 338 | &:active, 339 | &:focus { 340 | box-shadow: 0 0 0 1px ${theme.bg.default}, 341 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 342 | } 343 | 344 | @media (max-width: 768px) { 345 | width: calc(100% + 40px); 346 | border-radius: 4px; 347 | align-items: flex-start; 348 | margin-left: -24px; 349 | padding-left: 24px; 350 | 351 | &:hover, 352 | &:active, 353 | &:focus { 354 | background: ${theme.bg.default}!important; 355 | } 356 | 357 | .icon { 358 | margin-top: 1px; 359 | margin-left: 6px; 360 | margin-right: 18px !important; 361 | } 362 | } 363 | `; 364 | 365 | export const ResourceName = styled.p` 366 | font-size: 16px; 367 | font-weight: 400; 368 | `; 369 | 370 | export const Divider = styled.hr` 371 | border-bottom: 1px solid ${tint(theme.bg.wash, -4)}; 372 | margin-top: 24px; 373 | margin-bottom: 24px; 374 | height: 1px; 375 | width: 100%; 376 | 377 | @media (max-width: 768px) { 378 | display: none; 379 | } 380 | `; 381 | 382 | export const Content = styled.div` 383 | transition: max-height ${theme.animations.default}, 384 | opacity ${theme.animations.default}, visibility ${theme.animations.default}; 385 | 386 | max-height: 2000px; 387 | max-height: var(--maxHeight); 388 | opacity: 1; 389 | visibility: visible; 390 | 391 | &[aria-hidden='true'] { 392 | max-height: 0; 393 | opacity: 0; 394 | visibility: hidden; 395 | } 396 | `; 397 | 398 | export const Uncollapse = styled.button` 399 | background: ${theme.bg.wash}; 400 | border-radius: 20px; 401 | padding: 8px 16px; 402 | font-size: 15px; 403 | color: ${theme.text.tertiary}; 404 | cursor: pointer; 405 | line-height: 1; 406 | display: flex; 407 | align-items: center; 408 | justify-content: center; 409 | margin-right: 8px; 410 | margin-left: 16px; 411 | flex: 0 1 140px; 412 | 413 | @media (max-width: 768px) { 414 | display: none; 415 | } 416 | 417 | &:hover { 418 | color: ${theme.text.default}; 419 | background: ${tint(theme.bg.wash, -4)}; 420 | } 421 | &:active, 422 | &:focus { 423 | transition: all 0.2s ease-in-out; 424 | 425 | box-shadow: 0 0 0 1px ${theme.bg.default}, 426 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 427 | } 428 | `; 429 | 430 | export const OfferContainer = styled.a` 431 | display: flex; 432 | align-items: center; 433 | width: calc(100% - 8px); 434 | box-shadow: inset 0 0 1px ${theme.border.active}; 435 | border-radius: 4px; 436 | background: ${theme.bg.default}; 437 | padding: 8px 16px; 438 | position: relative; 439 | margin-top: 4px; 440 | margin-bottom: 8px; 441 | font-size: 14px; 442 | font-weight: 500; 443 | color: ${theme.text.tertiary}; 444 | overflow: hidden; 445 | 446 | .icon { 447 | margin-right: 4px; 448 | } 449 | 450 | @media (max-width: 768px) { 451 | align-items: flex-start; 452 | margin-top: 8px; 453 | width: 100%; 454 | 455 | .icon { 456 | display: none; 457 | } 458 | } 459 | 460 | &:hover { 461 | color: ${theme.text.secondary}; 462 | } 463 | &:active, 464 | &:focus { 465 | box-shadow: inset 0 0 1px ${theme.border.active}, 466 | 0 0 0 1px ${theme.bg.default}, 467 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 468 | } 469 | `; 470 | 471 | export const LeftBorder = styled.div` 472 | position: absolute; 473 | left: 0; 474 | top: 0; 475 | bottom: 0; 476 | height: 100px; 477 | width: 4px; 478 | background-image: linear-gradient(to bottom, #a913de, #6ac9ff); 479 | border-radius: 6px 0 0 6px; 480 | `; 481 | 482 | export const ExpandContainer = styled.div` 483 | display: flex; 484 | align-items: center; 485 | justify-content: center; 486 | width: calc(100% + 12px); 487 | position: relative; 488 | background: ${theme.bg.wash}; 489 | border: 1px solid ${tint(theme.bg.wash, -4)}; 490 | border-radius: 6px; 491 | padding: 8px 16px; 492 | margin-bottom: -28px; 493 | margin-top: 16px; 494 | margin-left: -16px; 495 | 496 | @media (max-width: 768px) { 497 | margin-bottom: 0; 498 | border-left: none; 499 | border-right: none; 500 | margin-top: -1px; 501 | border-radius: 0; 502 | margin-left: -24px; 503 | margin-right: -16px; 504 | width: calc(100% + 40px); 505 | } 506 | `; 507 | 508 | export const ExpandContent = styled.div` 509 | transition: max-height ${theme.animations.default}, 510 | opacity ${theme.animations.default}, visibility ${theme.animations.default}; 511 | max-height: 2000px; 512 | max-height: var(--maxHeight); 513 | opacity: 1; 514 | visibility: visible; 515 | 516 | &[aria-hidden='true'] { 517 | max-height: 0; 518 | opacity: 0; 519 | visibility: hidden; 520 | } 521 | `; 522 | -------------------------------------------------------------------------------- /components/ChecklistItem/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | getItemFromStorage, 4 | storeItem, 5 | removeItemFromStorage, 6 | } from '../../lib/localStorage'; 7 | 8 | export const getCheckedStatusById = (id: string) => getItemFromStorage(id); 9 | export const setCheckedStatusById = (id: string, store: boolean) => 10 | store ? storeItem(id, true) : removeItemFromStorage(id); 11 | -------------------------------------------------------------------------------- /components/Footer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Container, Description, Icons } from './style'; 4 | import Icon from '../Icon'; 5 | 6 | export default function Footer() { 7 | return ( 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | Copyright whenever. This is 21 | 26 | open source 27 | 28 | . 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/Footer/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | import { theme } from '../theme'; 4 | import { hexa } from '../globals'; 5 | 6 | export const Container = styled.div` 7 | margin-top: 128px; 8 | padding: 0 16px; 9 | width: 100%; 10 | `; 11 | 12 | export const Description = styled.p` 13 | font-size: 14px; 14 | color: ${theme.text.tertiary}; 15 | max-width: 320px; 16 | display: flex; 17 | flex: 1 0 auto; 18 | align-items: flex-start; 19 | padding-bottom: 16px; 20 | 21 | a { 22 | color: ${theme.text.default}; 23 | margin-left: 4px; 24 | } 25 | a:active, a:focus { 26 | box-shadow: 0 0 0 1px ${theme.bg.default}, 27 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 28 | } 29 | `; 30 | 31 | export const Icons = styled.div` 32 | display: flex; 33 | flex: 1 0 auto; 34 | align-items: flex-start; 35 | padding-bottom: 8px; 36 | 37 | a { 38 | color: ${theme.text.tertiary}; 39 | margin-right: 16px; 40 | line-height: 1; 41 | border-radius: 10px; 42 | height: 32px; 43 | } 44 | 45 | a:hover, a:active, a:focus { 46 | color: ${theme.text.default}; 47 | } 48 | 49 | a:active, a:focus { 50 | box-shadow: 0 0 0 1px ${theme.bg.default}, 51 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 52 | } 53 | `; 54 | -------------------------------------------------------------------------------- /components/Header/Confetti.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // The code in this component was based on Yoav Kadosh’s React Confetti 4 | // on Codepen : // https://codepen.io/ykadosh/pen/aaoZRB 5 | // distributed under the following license: 6 | // 7 | // Copyright (c) 2019 by Yoav Kadosh (https://codepen.io/ykadosh/pen/aaoZRB) 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | // IN THE SOFTWARE. 25 | 26 | import React, { useState, useRef, useEffect, Fragment } from 'react'; 27 | import { ParticleZone } from './style'; 28 | 29 | type ConfettiProps = { 30 | fireConfetti: boolean, 31 | }; 32 | type ParticlesProps = { 33 | count: number, 34 | }; 35 | type Props = {}; 36 | 37 | 38 | const COLORS = ['#2ecc71','#3498db','#e67e22','#e67e22','#e74c3c']; 39 | const TOP_OFFSET = typeof window != 'undefined' ? window.innerHeight : 0; 40 | const INNER_WIDTH = typeof window != 'undefined' ? window.innerWidth : 0; 41 | const LEFT_OFFSET = 250; 42 | 43 | const generateWholeNumber = (min, max) => min + Math.floor(Math.random()*(max - min)); 44 | 45 | const generateRandomColor = () => COLORS[generateWholeNumber(0,COLORS.length)]; 46 | 47 | export function CircularParticle(props: Props) { 48 | const currentCircularConfetti = useRef(null); 49 | 50 | const SIZE_RANGE = [5, 10]; 51 | const ROTATION_RANGE = [0, 45]; 52 | 53 | const size = generateWholeNumber(...SIZE_RANGE); 54 | const style = { 55 | backgroundColor: generateRandomColor(), 56 | width: size, 57 | height: size, 58 | borderRadius: size, 59 | transform: `rotateZ(${generateWholeNumber(...ROTATION_RANGE)}deg)`, 60 | left: generateWholeNumber(0, INNER_WIDTH), 61 | top: generateWholeNumber(-TOP_OFFSET, 0) 62 | }; 63 | 64 | useEffect(() => { 65 | const {left} = style; 66 | setTimeout(() => { 67 | const node = currentCircularConfetti.current; 68 | if (node) { 69 | node.style.top = window.innerHeight + generateWholeNumber(0, window.innerHeight) + 'px'; 70 | node.style.left = left + generateWholeNumber(-LEFT_OFFSET, LEFT_OFFSET) + 'px'; 71 | } 72 | },0); 73 | }); 74 | 75 | return ( 76 |
77 | ); 78 | } 79 | 80 | function SquiggleParticle(props: Props) { 81 | const currentSquiggleConfetti = useRef(null); 82 | const SIZE_RANGE = [15, 45]; 83 | const ROTATION_RANGE = [-15, 15]; 84 | 85 | const size = generateWholeNumber(...SIZE_RANGE); 86 | const style = { 87 | fill: generateRandomColor(), 88 | width: size, 89 | height: size, 90 | transform: `rotateZ(${generateWholeNumber(...ROTATION_RANGE)}deg)`, 91 | left: generateWholeNumber(0, INNER_WIDTH), 92 | top: generateWholeNumber(-TOP_OFFSET, 0) 93 | }; 94 | 95 | 96 | useEffect(() => { 97 | const {left} = style; 98 | setTimeout(() => { 99 | const node = currentSquiggleConfetti.current; 100 | if (node){ 101 | node.style.top = window.innerHeight + generateWholeNumber(0, window.innerHeight) + 'px'; 102 | node.style.left = left + generateWholeNumber(-LEFT_OFFSET, LEFT_OFFSET) + 'px'; 103 | node.style.transform = `rotateZ(${generateWholeNumber(...ROTATION_RANGE)}deg)`; 104 | } 105 | },0); 106 | }); 107 | 108 | return ( 109 |
110 | 115 | 116 | 117 |
118 | ); 119 | } 120 | 121 | export function Particles(props: ParticlesProps) { 122 | let {count: n} = props; 123 | const particles = []; 124 | const types = [SquiggleParticle, CircularParticle, CircularParticle]; 125 | 126 | while(n--) { 127 | const Particle = types[generateWholeNumber(0,3)]; 128 | particles.push( 129 | 130 | ); 131 | } 132 | 133 | return ( 134 | 135 | {particles} 136 | 137 | ); 138 | } 139 | 140 | 141 | export default function Confetti(props: ConfettiProps) { 142 | const [particles, setParticles] = useState([]); 143 | 144 | var id = 1; 145 | 146 | function clean(id) { 147 | setParticles(particles.filter(_id => _id !== id)) 148 | } 149 | 150 | useEffect(() => { 151 | if (props.fireConfetti) { 152 | id = id; 153 | id++; 154 | 155 | setParticles([...particles, id]) 156 | 157 | setTimeout(() => { 158 | clean(id); 159 | }, 5000); 160 | } 161 | }, 162 | [props.fireConfetti] 163 | ); 164 | 165 | return ( 166 | 167 | {props.fireConfetti && particles.map(id => ( 168 | 169 | ))} 170 | 171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /components/Header/Logo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | const Logo = () => ( 5 | 12 | 26 | 27 | 35 | 36 | 37 | 38 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | 53 | export default Logo; 54 | -------------------------------------------------------------------------------- /components/Header/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import Link from 'next/link'; 4 | import { 5 | Container, 6 | ButtonRowContainer, 7 | Label, 8 | LogoLink, 9 | Progression, 10 | ProgressBar, 11 | ProgressLabel } from './style'; 12 | import { PrimaryButton, GhostButton } from '../Button'; 13 | import Logo from './Logo'; 14 | import Confetti from './Confetti'; 15 | 16 | type Props = { 17 | showHeaderShadow: boolean, 18 | displayProgress: boolean, 19 | totalItemsCount: number, 20 | currentCount: number, 21 | }; 22 | 23 | export default function Header(props: Props) { 24 | const { showHeaderShadow, totalItemsCount, currentCount, displayProgress } = props; 25 | 26 | return ( 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | About 41 | 42 | 43 | 44 | 50 | Contribute 51 | 52 | 53 | 54 | { displayProgress && ( 55 | 60 | 0 ? false : true} 64 | /> 65 | 69 | { currentCount === totalItemsCount 70 | ? `🎉 Checklist complete! 🎉` 71 | : `${currentCount} of ${totalItemsCount} completed`} 72 | 73 | 74 | )} 75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /components/Header/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | import { theme } from '../theme'; 4 | import { hexa } from '../globals'; 5 | import { Shadows } from '../globals'; 6 | 7 | export const Container = styled.div` 8 | display: grid; 9 | grid-template-columns: 1fr 1fr; 10 | grid-template-areas: "logo actions"; 11 | padding: 16px 16px; 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | background: ${theme.bg.default}; 17 | z-index: 3; 18 | box-shadow: 0 4px 8px rgba(0,0,0,0.04); 19 | transition: all 0.2s ease-in-out; 20 | 21 | @media (max-width: 968px) { 22 | padding: 8px 16px; 23 | } 24 | `; 25 | 26 | export const Logo = styled.h1` 27 | grid-area: logo; 28 | font-size: 18px; 29 | font-weight: 700; 30 | color: ${theme.text.default}; 31 | `; 32 | 33 | export const Progression = styled.div` 34 | text-align: center; 35 | position: absolute; 36 | left: 0; 37 | right: 0; 38 | height: 32px; 39 | width: 100%; 40 | bottom: -16px; 41 | display: block; 42 | background: transparent; 43 | 44 | &:active, &:focus { 45 | outline: none; 46 | } 47 | `; 48 | 49 | export const ProgressBar = styled.div` 50 | display: block; 51 | height: 4px; 52 | width: 100%; 53 | margin: 16px 0 0; 54 | position: relative; 55 | overflow: hidden; 56 | background-image: linear-gradient(to left, #a913de, #6ac9ff); 57 | box-shadow: 0 4px 8px rgba(0,0,0,0.04); 58 | z-index: 5; 59 | 60 | ${Progression}:focus &, 61 | ${Progression}:active & { 62 | box-shadow: 0 0 0 1px ${theme.bg.default}, 63 | 0 0 0 3px ${props => hexa(props.theme.brand.default, 0.25)}; 64 | } 65 | 66 | &::after { 67 | content: ''; 68 | position: absolute; 69 | height: 100%; 70 | top: 0; 71 | right: 0; 72 | width: 100%; 73 | background: ${theme.border.default}; 74 | max-width: var(--progress); 75 | transition: max-width ${theme.animations.default}, background ${theme.animations.default}; 76 | } 77 | &[disabled]::after { 78 | background: ${theme.bg.default}; 79 | } 80 | `; 81 | 82 | export const ProgressLabel = styled.p` 83 | visibility: hidden; 84 | opacity: 0; 85 | position: absolute; 86 | z-index: 4; 87 | bottom: -25%; 88 | left: 50%; 89 | transform: translateX(-50%); 90 | background: ${theme.bg.default}; 91 | padding: 8px 16px; 92 | font-size: 14px; 93 | font-weight: 600; 94 | 95 | transition: all ${theme.animations.default}; 96 | 97 | border-radius: 8px; 98 | white-space: nowrap; 99 | ${Shadows.default}; 100 | 101 | ${Progression}:hover ${ProgressBar}:not([disabled]) + &, 102 | ${Progression}:focus ${ProgressBar}:not([disabled]) + &, 103 | ${Progression}:active ${ProgressBar}:not([disabled]) + & 104 | { 105 | visibility: visible; 106 | opacity: 1; 107 | bottom: -100%; 108 | } 109 | 110 | @media (max-width: 968px) { 111 | padding: 8px 12px; 112 | } 113 | ` 114 | 115 | export const ButtonRowContainer = styled.div` 116 | display: flex; 117 | justify-content: flex-end; 118 | grid-area: actions; 119 | align-items: center; 120 | 121 | a { 122 | margin-left: 8px; 123 | } 124 | `; 125 | 126 | export const LogoLink = styled.a` 127 | transition: all ${props => props.theme.animations.default}; 128 | display: inline-flex; 129 | align-items: center; 130 | border-radius: 6px; 131 | height: 100%; 132 | 133 | &:hover { 134 | transform: scale(1.2); 135 | } 136 | &:active, &:focus { 137 | transform: scale(1.2); 138 | box-shadow: 0 0 0 1px ${theme.bg.default}, 139 | 0 0 0 3px ${props => hexa(props.theme.text.tertiary, 0.25)}; 140 | } 141 | `; 142 | 143 | export const Label = styled.h1` 144 | position: absolute; 145 | left: -9999px; 146 | visibility: none; 147 | `; 148 | 149 | export const ParticleZone = styled.div` 150 | position: fixed; 151 | top: 0; 152 | left: 0; 153 | right: 0; 154 | bottom: 0; 155 | 156 | & > div, & > svg { 157 | position: absolute; 158 | transition: all 5s ease-out; 159 | } 160 | ` 161 | 162 | -------------------------------------------------------------------------------- /components/LoadingChecklistItem/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { ShimmerInboxThread, ShimmerBase, ShimmerLine, Cover } from './style'; 4 | import Card from '../Card'; 5 | 6 | const LoadingChecklistItem = () => ( 7 |
8 | 9 | 10 | 11 | 12 | 20 | 28 | 29 | 37 | 38 | 46 | 54 | 62 | 70 | 71 | 72 | 73 |
74 | ); 75 | 76 | export default LoadingChecklistItem; 77 | -------------------------------------------------------------------------------- /components/LoadingChecklistItem/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled, { keyframes } from 'styled-components'; 3 | import { theme } from '../theme'; 4 | 5 | const placeHolderShimmer = keyframes` 6 | 0%{ 7 | transform: translateX(-100%) translateY(0%); 8 | background-size: 100%; 9 | opacity: 1; 10 | } 11 | 100%{ 12 | transform: translateX(200%) translateY(0%); 13 | background-size: 500%; 14 | opacity: 0; 15 | } 16 | `; 17 | 18 | export const ShimmerInboxThread = styled.div` 19 | padding: 16px; 20 | 21 | section { 22 | min-height: 40px; 23 | } 24 | 25 | &:last-of-type { 26 | border-bottom: 0; 27 | } 28 | `; 29 | 30 | export const ShimmerBase = styled.section` 31 | width: 100%; 32 | height: 100%; 33 | position: relative; 34 | background: ${theme.bg.wash}; 35 | overflow: hidden; 36 | `; 37 | 38 | export const ShimmerLine = styled.span` 39 | width: 100%; 40 | height: 100%; 41 | position: absolute; 42 | animation-duration: 2.5s; 43 | animation-fill-mode: forwards; 44 | animation-iteration-count: infinite; 45 | animation-timing-function: ease-in-out; 46 | background: linear-gradient( 47 | to right, 48 | ${theme.bg.wash} 10%, 49 | ${theme.border.active}, 50 | ${theme.bg.wash} 30% 51 | ); 52 | animation-name: ${placeHolderShimmer}; 53 | `; 54 | 55 | export const Cover = styled.span` 56 | position: absolute; 57 | background: ${theme.bg.default}; 58 | `; 59 | -------------------------------------------------------------------------------- /components/Page/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowIssue 3 | import React, { useState, useEffect } from 'react'; 4 | import type { Node } from 'react'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import { throttle } from 'throttle-debounce'; 7 | import Icon from '../Icon'; 8 | import Header from '../Header'; 9 | import Footer from '../Footer'; 10 | import { theme } from '../theme'; 11 | import { 12 | Container, 13 | SectionHeading, 14 | Heading, 15 | Subheading, 16 | InnerContainer, 17 | ScrollToTop, 18 | } from './style'; 19 | import { getLocalStorageLength } from '../../lib/localStorage'; 20 | import data from '../../config/data'; 21 | 22 | export { SectionHeading, Heading, Subheading }; 23 | 24 | type Props = { 25 | children: Node, 26 | displayProgress: boolean, 27 | }; 28 | 29 | const totalItemsCount = Object.keys(data).length; 30 | 31 | export default function Page(props: Props) { 32 | const { children, displayProgress } = props; 33 | const [showHeaderShadow, setHeaderShadow] = useState(false); 34 | const [scrollToTopVisible, setScrollToTopVisible] = useState(false); 35 | const [progress, setProgress] = useState(0); 36 | const [currentCount, setCurrentCount] = useState(0); 37 | 38 | function handleScroll() { 39 | const headerShadowState = window && window.pageYOffset > 0; 40 | const scrollToTopState = window && window.pageYOffset > 240; 41 | setHeaderShadow(headerShadowState); 42 | setScrollToTopVisible(scrollToTopState); 43 | } 44 | 45 | const throttledScroll = throttle(300, handleScroll); 46 | 47 | function updateProgress() { 48 | const checkedItemsCount = getLocalStorageLength(); 49 | const progressPercentage = (checkedItemsCount * 100) / totalItemsCount; 50 | setProgress(progressPercentage); 51 | setCurrentCount(checkedItemsCount); 52 | } 53 | 54 | const scrollToTop = () => { 55 | if (window) { 56 | window.scrollTo(0, 0); 57 | } 58 | }; 59 | 60 | useEffect(() => { 61 | updateProgress(); 62 | 63 | if (window) { 64 | window.addEventListener('scroll', throttledScroll); 65 | } 66 | 67 | return () => { 68 | if (window) { 69 | window.removeEventListener('scroll', throttledScroll); 70 | } 71 | }; 72 | }, [progress]); 73 | 74 | useEffect(() => { 75 | if (window && displayProgress) { 76 | window.addEventListener('storage:updated', updateProgress); 77 | } 78 | return () => { 79 | if (window && displayProgress) { 80 | window.removeEventListener('storage:updated', updateProgress); 81 | } 82 | }; 83 | }); 84 | 85 | return ( 86 | 87 | 88 | 94 | 95 |
101 | {children} 102 |