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,
+};