diff --git a/components/Card/index.js b/components/Card/index.js index 0be2ca7..ac88284 100644 --- a/components/Card/index.js +++ b/components/Card/index.js @@ -8,6 +8,10 @@ type Props = { }; export default function Card(props: Props) { - const { style, children } = props; - return {children}; + const { style, children, ...rest } = props; + return ( + + {children} + + ); } diff --git a/components/Card/style.js b/components/Card/style.js index 109117d..2f789ae 100644 --- a/components/Card/style.js +++ b/components/Card/style.js @@ -7,12 +7,4 @@ export const StyledCard = styled.div` background: ${props => props.theme.bg.default}; border-radius: 6px; ${Shadows.default}; - - &:hover { - ${Shadows.hover}; - } - - &:active { - ${Shadows.active}; - } `; diff --git a/components/Checklist/index.js b/components/Checklist/index.js new file mode 100644 index 0000000..41c8f81 --- /dev/null +++ b/components/Checklist/index.js @@ -0,0 +1,21 @@ +// @flow +import React from 'react'; +import data from '../../config/data'; +import ChecklistItem from '../ChecklistItem'; +import { Grid } from './style'; + +class Checklist extends React.Component<{}> { + render() { + const keys = Object.keys(data); + const resources = keys.map(k => data[k]); + return ( + + {resources.map(resource => ( + + ))} + + ); + } +} + +export default Checklist; diff --git a/components/Checklist/style.js b/components/Checklist/style.js new file mode 100644 index 0000000..535ac4f --- /dev/null +++ b/components/Checklist/style.js @@ -0,0 +1,9 @@ +// @flow +import styled from 'styled-components'; + +export const Grid = styled.div` + display: grid; + grid-template-columns: 1fr; + margin-top: 32px; + width: 100%; +`; diff --git a/components/ChecklistItem/App.js b/components/ChecklistItem/App.js new file mode 100644 index 0000000..bb477ac --- /dev/null +++ b/components/ChecklistItem/App.js @@ -0,0 +1,82 @@ +// @flow +import React from 'react'; +import type { App } from '../../types'; +import { + AppMeta, + AppRowContainer, + AppIcon, + AppName, + AppSourcesList, + AppSourcesListItem, + AppSourcesLabel, +} from './style'; +import Icon from '../Icon'; + +type Props = { + app: App, +}; + +export const AppRow = ({ app }: Props) => { + const sourcesKeys = Object.keys(app.sources); + + const renderSourceIcon = (key: string) => { + const sourceUrl = app.sources[key]; + const appUrl = app.url; + + if (key === 'windows') { + return ( + + + + Windows + + + ); + } + + if (key === 'ios') { + return ( + + + + iOS + + + ); + } + + if (key === 'macos') { + return ( + + + + macOS + + + ); + } + + if (key === 'android') { + return ( + + + + Android + + + ); + } + + return null; + }; + + return ( + + + + {app.name} + + {sourcesKeys.map(renderSourceIcon)} + + ); +}; diff --git a/components/ChecklistItem/Apps.js b/components/ChecklistItem/Apps.js new file mode 100644 index 0000000..608b28a --- /dev/null +++ b/components/ChecklistItem/Apps.js @@ -0,0 +1,20 @@ +// @flow +import React from 'react'; +import { AppsContainer } from './style'; +import type { ChecklistResource } from '../../types'; +import { AppRow } from './App'; + +type Props = { + resource: ChecklistResource, +}; + +export const Apps = ({ resource }: Props) => { + if (!resource.apps) return null; + return ( + + {resource.apps.map(app => ( + + ))} + + ); +}; diff --git a/components/ChecklistItem/Heading.js b/components/ChecklistItem/Heading.js new file mode 100644 index 0000000..7e3add0 --- /dev/null +++ b/components/ChecklistItem/Heading.js @@ -0,0 +1,16 @@ +// @flow +import React from 'react'; +import { Title, Description } from './style'; +import type { ChecklistResource } from '../../types'; + +type Props = { + resource: ChecklistResource, + isChecked: boolean, +}; + +export const Heading = ({ resource, isChecked }: Props) => ( + + {resource.title} + {resource.description} + +); diff --git a/components/ChecklistItem/Resource.js b/components/ChecklistItem/Resource.js new file mode 100644 index 0000000..dd20fff --- /dev/null +++ b/components/ChecklistItem/Resource.js @@ -0,0 +1,20 @@ +// @flow +import React from 'react'; +import type { Resource } from '../../types'; +import { ResourceRowContainer, ResourceName } from './style'; +import Icon from '../Icon'; + +type Props = { + resource: Resource, +}; + +export const ResourceRow = ({ resource }: Props) => ( + + + {resource.name} + +); diff --git a/components/ChecklistItem/Resources.js b/components/ChecklistItem/Resources.js new file mode 100644 index 0000000..bd9f42b --- /dev/null +++ b/components/ChecklistItem/Resources.js @@ -0,0 +1,21 @@ +// @flow +import React from 'react'; +import { SectionHeading } from './style'; +import type { ChecklistResource } from '../../types'; +import { ResourceRow } from './Resource'; + +type Props = { + resource: ChecklistResource, +}; + +export const Resources = ({ resource }: Props) => { + if (!resource.resources) return null; + return ( + + More Resources + {resource.resources.map(r => ( + + ))} + + ); +}; diff --git a/components/ChecklistItem/index.js b/components/ChecklistItem/index.js new file mode 100644 index 0000000..5d51c14 --- /dev/null +++ b/components/ChecklistItem/index.js @@ -0,0 +1,69 @@ +// @flow +import React from 'react'; +import type { ChecklistResource } from '../../types'; +import { getCheckedStatusById, setCheckedStatusById } from './utils'; +import Card from '../Card'; +import LoadingChecklistItem from '../LoadingChecklistItem'; +import { Heading } from './Heading'; +import { Apps } from './Apps'; +import { Resources } from './Resources'; +import { + Container, + CheckboxContainer, + Checkbox, + CardContent, + ResourceContent, +} from './style'; + +type Props = { + resource: ChecklistResource, +}; + +type State = { + isChecked: boolean, + isLoading: boolean, +}; + +class ChecklistItem extends React.Component { + state = { isChecked: false, isLoading: true }; + + componentDidMount() { + const { resource } = this.props; + const isChecked = getCheckedStatusById(resource.id); + return this.setState({ isChecked, isLoading: false }); + } + + handleSetChecked = () => { + const { isChecked } = this.state; + const { resource } = this.props; + setCheckedStatusById(resource.id, !isChecked); + return this.setState({ isChecked: !isChecked }); + }; + + render() { + const { isChecked, isLoading } = this.state; + const { resource } = this.props; + + if (isLoading) return ; + + return ( + + + + + + + + + + + + + + + + ); + } +} + +export default ChecklistItem; diff --git a/components/ChecklistItem/style.js b/components/ChecklistItem/style.js new file mode 100644 index 0000000..f518ed8 --- /dev/null +++ b/components/ChecklistItem/style.js @@ -0,0 +1,240 @@ +// @flow +import styled from 'styled-components'; +import { theme } from '../theme'; +import { Shadows } from '../globals'; + +export const Container = styled.div` + margin-bottom: 24px; +`; + +export const CardContent = styled.div` + padding: 24px 16px; + display: flex; + width: 100%; + + @media (max-width: 768px) { + flex-direction: column; + } +`; + +export const Title = styled.h3` + font-size: 18px; + font-weight: 500; + color: ${theme.text.default}; +`; + +export const Description = styled.h4` + font-size: 16px; + font-weight: 400; + color: ${theme.text.secondary}; +`; + +export const SectionHeading = styled.h5` + font-size: 12px; + font-weight: 500; + color: ${theme.text.tertiary}; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 32px; +`; + +export const CheckboxContainer = styled.div` + display: flex; + align-items: flex-start; + width: 48px; + justify-content: center; +`; + +export const Checkbox = styled.span` + width: 32px; + height: 32px; + border-radius: 4px; + border: 1px solid + ${props => (props.isChecked ? theme.brand.default : theme.border.default)}; + background: ${props => + props.isChecked ? theme.brand.default : theme.bg.wash}; + cursor: pointer; + position: relative; + + &:hover { + ${Shadows.default}; + border: 1px solid + ${props => (props.isChecked ? theme.brand.default : theme.border.active)}; + } + + &:after { + content: ''; + position: absolute; + display: block; + left: 11px; + top: 6px; + width: 6px; + height: 12px; + border: solid + ${props => (props.isChecked ? theme.bg.default : theme.border.active)}; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } +`; + +export const ResourceContent = styled.div` + display: flex; + flex-direction: column; + padding-left: 16px; + width: 100%; + + @media (max-width: 768px) { + padding-left: 8px; + margin-top: 24px; + } +`; + +export const AppsContainer = styled.div` + margin-top: 8px; + + @media (max-width: 768px) { + margin-top: 24px; + } +`; + +export const AppMeta = styled.div` + display: flex; + align-items: center; + + @media (max-width: 768px) { + width: 100%; + padding-bottom: 16px; + margin-bottom: 16px; + } +`; + +export const AppRowContainer = styled.a` + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; + width: 100%; + border-radius: 6px; + padding: 8px; + padding-left: 16px; + margin-left: -16px; + width: calc(100% + 12px); + flex-wrap: wrap; + + @media (max-width: 768px) { + border-top: 1px solid ${theme.bg.wash}; + border-radius: 0; + margin-top: 0px; + margin-left: -24px; + width: calc(100% + 40px); + padding: 24px; + + &:last-of-type { + border-bottom: 1px solid ${theme.bg.wash}; + } + } + + &:hover { + background: ${theme.bg.wash}; + } +`; + +export const AppIcon = styled.img` + width: 40px; + height: 40px; + border-radius: 8px; + margin-right: 12px; +`; + +export const AppName = styled.p` + font-size: 16px; + font-weight: 400; + color: ${theme.text.secondary}; +`; + +export const AppSourcesList = styled.ul` + list-style-type: none; + display: flex; + align-items: center; + margin-right: 8px; + + @media (max-width: 768px) { + width: 100%; + margin-right: 0; + justify-content: space-between; + } +`; + +export const AppSourcesListItem = styled.li` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: ${theme.text.tertiary}; + margin-left: 4px; + padding: 4px 8px; + border-radius: 4px; + min-width: 56px; + transition: all 0.1s ease-in-out; + + a { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .icon { + position: relative; + top: 4px; + left: 6px; + } + + &:hover { + color: ${theme.text.secondary}; + background: ${theme.bg.default}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); + transition: all 0.1s ease-in-out; + } +`; + +export const AppSourcesLabel = styled.span` + font-size: 12px; + font-weight: 400; + color: ${theme.text.tertiary}; + margin-top: 4px; +`; + +export const ResourceRowContainer = styled.a` + display: flex; + align-items: center; + margin-top: 4px; + width: 100%; + border-radius: 6px; + padding: 8px 12px; + margin-left: -12px; + color: ${theme.text.tertiary}; + width: calc(100% + 8px); + + @media (max-width: 768px) { + width: calc(100% + 16px); + } + + &:first-of-type { + margin-top: 8px; + } + + .icon { + margin-right: 8px; + } + + &:hover { + background: ${theme.bg.wash}; + color: ${theme.text.default}; + } +`; + +export const ResourceName = styled.p` + font-size: 16px; + font-weight: 400; +`; diff --git a/components/ChecklistItem/utils.js b/components/ChecklistItem/utils.js new file mode 100644 index 0000000..7058f71 --- /dev/null +++ b/components/ChecklistItem/utils.js @@ -0,0 +1,10 @@ +// @flow +import { + getItemFromStorage, + storeItem, + removeItemFromStorage, +} from '../../lib/localStorage'; + +export const getCheckedStatusById = (id: string) => getItemFromStorage(id); +export const setCheckedStatusById = (id: string, store: boolean) => + store ? storeItem(id, true) : removeItemFromStorage(id); diff --git a/components/Icon/index.js b/components/Icon/index.js index 1325476..7f8bbba 100644 --- a/components/Icon/index.js +++ b/components/Icon/index.js @@ -72,6 +72,33 @@ export const Glyph = ({ glyph }: GlyphProps): any => { ); + case 'apple': + return ( + + + + ); + case 'app-store': + return ( + + + + ); + case 'windows': + return ( + + + + ); + case 'android': + return ( + + + + + + + ); default: return null; } diff --git a/components/LoadingChecklistItem/index.js b/components/LoadingChecklistItem/index.js new file mode 100644 index 0000000..c1ec06a --- /dev/null +++ b/components/LoadingChecklistItem/index.js @@ -0,0 +1,74 @@ +// @flow +import React from 'react'; +import { ShimmerInboxThread, ShimmerBase, ShimmerLine, Cover } from './style'; +import Card from '../Card'; + +const LoadingChecklistItem = () => ( + + + + + + + + + + + + + + + + +); + +export default LoadingChecklistItem; diff --git a/components/LoadingChecklistItem/style.js b/components/LoadingChecklistItem/style.js new file mode 100644 index 0000000..c8b7d71 --- /dev/null +++ b/components/LoadingChecklistItem/style.js @@ -0,0 +1,58 @@ +// @flow +import styled, { keyframes } from 'styled-components'; +import { theme } from '../theme'; + +const placeHolderShimmer = keyframes` + 0%{ + transform: translateX(-100%) translateY(0%); + background-size: 100%; + opacity: 1; + } + 100%{ + transform: translateX(200%) translateY(0%); + background-size: 500%; + opacity: 0; + } +`; + +export const ShimmerInboxThread = styled.div` + padding: 16px; + + section { + min-height: 40px; + } + + &:last-of-type { + border-bottom: 0; + } +`; + +export const ShimmerBase = styled.section` + width: 100%; + height: 100%; + position: relative; + background: ${theme.bg.wash}; + overflow: hidden; +`; + +export const ShimmerLine = styled.span` + width: 100%; + height: 100%; + position: absolute; + animation-duration: 2.5s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + background: linear-gradient( + to right, + ${theme.bg.wash} 10%, + ${theme.border.active}, + ${theme.bg.wash} 30% + ); + animation-name: ${placeHolderShimmer}; +`; + +export const Cover = styled.span` + position: absolute; + background: ${theme.bg.default}; +`; diff --git a/components/Page/style.js b/components/Page/style.js index eed1b1a..eac5938 100644 --- a/components/Page/style.js +++ b/components/Page/style.js @@ -48,8 +48,9 @@ export const SectionHeading = styled.div` `; export const Heading = styled.h3` - font-size: 24px; + font-size: 32px; font-weight: 700; + line-height: 1.2; color: ${theme.text.default}; @media (max-width: 968px) { @@ -58,9 +59,10 @@ export const Heading = styled.h3` `; export const Subheading = styled.h4` - font-size: 18px; + font-size: 22px; font-weight: 400; color: ${theme.text.tertiary}; + margin-top: 8px; a { color: ${theme.text.default}; diff --git a/config/data.js b/config/data.js new file mode 100644 index 0000000..afb4ce4 --- /dev/null +++ b/config/data.js @@ -0,0 +1,5 @@ +import passwordManager from './passwordManager'; + +export default { + passwordManager, +}; diff --git a/config/passwordManager.js b/config/passwordManager.js new file mode 100644 index 0000000..a9f8a4e --- /dev/null +++ b/config/passwordManager.js @@ -0,0 +1,47 @@ +export default { + id: 'passwordManager', + title: 'Password Manager', + description: + 'A password manager helps you set unique passwords for each service you use. Having a unique password ensures that if one service you use is hacked, the leaked password won’t allow access to all of your accounts.', + apps: [ + { + name: '1Password', + image: '/static/img/1password.jpg', + url: 'https://1password.com/', + sources: { + windows: 'https://1password.com/downloads/windows/', + ios: 'https://1password.com/downloads/ios/', + android: 'https://1password.com/downloads/android/', + macos: 'https://1password.com/downloads/mac/', + website: 'https://1password.com/', + }, + }, + { + name: 'LastPass', + image: '/static/img/lastpass.jpg', + url: 'https://www.lastpass.com/', + sources: { + windows: + 'https://download.cloud.lastpass.com/windows_installer/lastpass.exe', + ios: + 'https://itunes.apple.com/us/app/lastpass-password-manager/id324613447', + android: 'https://lastpass.com/android_market.php', + macos: + 'https://itunes.apple.com/us/app/lastpass/id926036361?ls=1&mt=12', + website: 'https://www.lastpass.com/', + }, + }, + ], + resources: [ + { + name: 'How to use a password manager (and why you really should)', + url: + 'https://www.theverge.com/2017/7/24/15921282/best-password-manager-1password-lastpass-dashlane-how-to', + }, + { + name: 'How password managers work and why you should use one', + url: + 'https://motherboard.vice.com/en_us/article/59yv5x/how-password-managers-work-and-why-you-should-use-one', + }, + ], +}; diff --git a/pages/index.js b/pages/index.js index 02a101a..c442194 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,7 +1,8 @@ // @flow import * as React from 'react'; -import Page, { SectionHeading } from '../components/Page'; +import Page, { SectionHeading, Heading, Subheading } from '../components/Page'; import type { GetInitialProps } from '../types'; +import Checklist from '../components/Checklist'; class Index extends React.Component<{}> { static async getInitialProps({ res }: GetInitialProps) { @@ -17,7 +18,15 @@ class Index extends React.Component<{}> { render() { return ( - + + Improve your online privacy and security + + The follow resources are designed to improve everyday people’s + security and privacy while using the internet. + + + + ); } diff --git a/static/img/1password.jpg b/static/img/1password.jpg new file mode 100644 index 0000000..a1bd7e1 Binary files /dev/null and b/static/img/1password.jpg differ diff --git a/static/img/lastpass.jpg b/static/img/lastpass.jpg new file mode 100644 index 0000000..9fa0a33 Binary files /dev/null and b/static/img/lastpass.jpg differ diff --git a/types/index.js b/types/index.js index 2766434..dc9e790 100644 --- a/types/index.js +++ b/types/index.js @@ -11,3 +11,31 @@ export type GetInitialProps = { getInitialProps?: (ctx: any) => void, }, }; + +export type Resource = { + name: string, + url: string, +}; + +export type AppSource = { + windows?: string, + ios?: string, + macos?: string, + anrdroid?: string, + website?: string, +}; + +export type App = { + name: string, + image: string, + sources: AppSource, + url: string, +}; + +export type ChecklistResource = { + id: string, + title: string, + description: string, + apps?: ?Array, + resources?: ?Array, +};