diff --git a/.stylelintrc b/.stylelintrc
index 6d8cca69e4..75adc14e70 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -5,5 +5,16 @@
"extends": [
"stylelint-config-recommended",
"stylelint-config-styled-components"
- ]
+ ],
+ "rules": {
+ "selector-type-no-unknown": [
+ true,
+ {
+ "ignoreTypes": [
+ "/-styled-mixin/",
+ "$dummyValue"
+ ]
+ }
+ ]
+ }
}
diff --git a/__tests__/CommentActionButtons.tsx b/__tests__/CommentActionButtons.tsx
index 1548b9f47b..3308af6380 100644
--- a/__tests__/CommentActionButtons.tsx
+++ b/__tests__/CommentActionButtons.tsx
@@ -60,7 +60,15 @@ const renderComponent = (
return render(
-
+
,
diff --git a/__tests__/MainComment.tsx b/__tests__/MainComment.tsx
index 20399dc600..6b5a61b99b 100644
--- a/__tests__/MainComment.tsx
+++ b/__tests__/MainComment.tsx
@@ -50,7 +50,13 @@ const renderLayout = (
return render(
diff --git a/__tests__/MainLayout.tsx b/__tests__/MainLayout.tsx
index 3448cc5d82..8fdac9fa21 100644
--- a/__tests__/MainLayout.tsx
+++ b/__tests__/MainLayout.tsx
@@ -12,7 +12,15 @@ beforeEach(() => {
const renderLayout = (user: LoggedUser = null): RenderResult => {
return render(
-
+
,
);
diff --git a/__tests__/NewCommentModal.tsx b/__tests__/NewCommentModal.tsx
index 2e849556b5..a2642ebb0c 100644
--- a/__tests__/NewCommentModal.tsx
+++ b/__tests__/NewCommentModal.tsx
@@ -49,6 +49,8 @@ const renderComponent = (
user: { ...defaultUser, ...user },
shouldShowLogin: false,
showLogin: jest.fn(),
+ showProfile: jest.fn(),
+ logout: jest.fn(),
}}
>
diff --git a/__tests__/PostPage.tsx b/__tests__/PostPage.tsx
index 7dd88e9d14..019a8bdec8 100644
--- a/__tests__/PostPage.tsx
+++ b/__tests__/PostPage.tsx
@@ -105,7 +105,15 @@ const renderPost = (
return render(
-
+
,
diff --git a/__tests__/ConfirmAccountModal.tsx b/__tests__/ProfileModal.tsx
similarity index 76%
rename from __tests__/ConfirmAccountModal.tsx
rename to __tests__/ProfileModal.tsx
index 0e15d7002c..403222f42d 100644
--- a/__tests__/ConfirmAccountModal.tsx
+++ b/__tests__/ProfileModal.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import { mocked } from 'ts-jest/utils';
-import ConfirmAccountModal, { Props } from '../components/ConfirmAccountModal';
+import ProfileModal, { Props } from '../components/ProfileModal';
import AuthContext from '../components/AuthContext';
import { LoggedUser, updateProfile } from '../lib/user';
@@ -11,10 +11,10 @@ jest.mock('../lib/user', () => ({
}));
const profiledUpdated = jest.fn();
+const logout = jest.fn();
beforeEach(() => {
- mocked(updateProfile).mockReset();
- profiledUpdated.mockReset();
+ jest.resetAllMocks();
});
const defaultUser = {
@@ -35,6 +35,7 @@ const renderComponent = (
isOpen: true,
profiledUpdated,
ariaHideApp: false,
+ confirmMode: false,
};
return render(
@@ -43,29 +44,32 @@ const renderComponent = (
user: { ...defaultUser, ...user },
shouldShowLogin: false,
showLogin: jest.fn(),
+ showProfile: jest.fn(),
+ logout,
}}
>
-
+
,
);
};
it('should enable submit when form is valid', () => {
renderComponent();
- const el = screen.getByText('Confirm');
+ const el = screen.getByText('Save changes');
expect(el.getAttribute('disabled')).toBeFalsy();
});
it('should submit information on button click', async () => {
renderComponent();
mocked(updateProfile).mockResolvedValue(defaultUser);
- screen.getByText('Confirm').click();
+ screen.getByText('Save changes').click();
await waitFor(() => expect(profiledUpdated).toBeCalledWith(defaultUser));
expect(updateProfile).toBeCalledWith({
name: 'Ido Shamun',
email: 'ido@acme.com',
company: null,
title: null,
+ acceptedMarketing: false,
});
});
@@ -76,15 +80,22 @@ it('should show server error', async () => {
code: 1,
message: '',
});
- screen.getByText('Confirm').click();
+ screen.getByText('Save changes').click();
await waitFor(() =>
expect(updateProfile).toBeCalledWith({
name: 'Ido Shamun',
email: 'ido@acme.com',
company: null,
title: null,
+ acceptedMarketing: false,
}),
);
expect(profiledUpdated).toBeCalledTimes(0);
expect(screen.getByRole('alert')).toBeInTheDocument();
});
+
+it('should logout on button click', async () => {
+ renderComponent();
+ screen.getByText('Logout').click();
+ await waitFor(() => expect(logout).toBeCalledTimes(1));
+});
diff --git a/__tests__/SubComment.tsx b/__tests__/SubComment.tsx
index bf21de672b..339df6f247 100644
--- a/__tests__/SubComment.tsx
+++ b/__tests__/SubComment.tsx
@@ -50,7 +50,13 @@ const renderLayout = (
return render(
diff --git a/components/AuthContext.ts b/components/AuthContext.ts
index ece747b4e9..c81024f474 100644
--- a/components/AuthContext.ts
+++ b/components/AuthContext.ts
@@ -5,6 +5,8 @@ interface AuthContextData {
user: LoggedUser;
shouldShowLogin: boolean;
showLogin: () => void;
+ showProfile: () => void;
+ logout: () => Promise;
}
const AuthContext = React.createContext(null);
diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx
index 69f20707cb..beee54c735 100644
--- a/components/LoginModal.tsx
+++ b/components/LoginModal.tsx
@@ -5,9 +5,11 @@ import DailyDevLogo from './DailyDevLogo';
import { InvertButton, TextButton } from './Buttons';
import XIcon from '../icons/x.svg';
import GitHubIcon from '../icons/github.svg';
-import { typoJr, typoMicro2Base } from '../styles/typography';
+import { typoJr } from '../styles/typography';
import { CodeChallenge, generateChallenge } from '../lib/auth';
import { StyledModal, ModalCloseButton, Props } from './StyledModal';
+import { privacyPolicy, termsOfService } from '../lib/constants';
+import { LegalNotice } from './utilities';
const MyModal = styled(StyledModal)`
.Modal {
@@ -17,6 +19,11 @@ const MyModal = styled(StyledModal)`
width: 9.25rem;
}
}
+
+ ${LegalNotice} {
+ max-width: 17.25rem;
+ margin-top: ${size8};
+ }
`;
const Buttons = styled.div`
@@ -44,24 +51,6 @@ const Content = styled.div`
${typoJr}
`;
-const LegalNotice = styled.div`
- max-width: 17.25rem;
- margin-top: ${size8};
- color: var(--theme-disabled);
- text-align: center;
- font-weight: bold;
- ${typoMicro2Base};
-
- a {
- display: inline-block;
- text-decoration: underline;
- color: inherit;
- @supports (display: contents) {
- display: contents;
- }
- }
-`;
-
export default function LoginModal(props: Props): ReactElement {
// eslint-disable-next-line react/prop-types
const { onRequestClose } = props;
@@ -109,19 +98,11 @@ export default function LoginModal(props: Props): ReactElement {
By signing up I accept the{' '}
-
+
Terms of Service
{' '}
and the{' '}
-
+
Privacy Policy
.
diff --git a/components/MainLayout.tsx b/components/MainLayout.tsx
index 3af3a7dbb7..9b47e6595b 100644
--- a/components/MainLayout.tsx
+++ b/components/MainLayout.tsx
@@ -51,6 +51,7 @@ const ProfileImage = styled.button`
background: none;
border: none;
border-radius: 100%;
+ cursor: pointer;
${focusOutline}
`;
@@ -69,7 +70,7 @@ const HomeLink = styled.a`
`;
export default function MainLayout({ children }: Props): ReactElement {
- const { user, showLogin } = useContext(AuthContext);
+ const { user, showLogin, showProfile } = useContext(AuthContext);
return (
@@ -79,7 +80,7 @@ export default function MainLayout({ children }: Props): ReactElement {
{user ? (
-
+
void;
}
-export default function ConfirmAccountModal(props: Props): ReactElement {
+export default function ProfileModal(props: Props): ReactElement {
// eslint-disable-next-line react/prop-types
- const { onRequestClose } = props;
- const { user } = useContext(AuthContext);
+ const { onRequestClose, confirmMode } = props;
+ const { user, logout } = useContext(AuthContext);
const formRef = useRef(null);
const [disableSubmit, setDisableSubmit] = useState(false);
@@ -120,9 +151,13 @@ export default function ConfirmAccountModal(props: Props): ReactElement {
if (val.name === '') {
return acc;
}
- return Object.assign(acc, {
+ if (val.type === 'checkbox') {
+ return { ...acc, [val.name]: val.checked };
+ }
+ return {
+ ...acc,
[val.name]: val.value.length ? val.value : null,
- });
+ };
},
{ name: '', email: '' },
);
@@ -146,8 +181,12 @@ export default function ConfirmAccountModal(props: Props): ReactElement {
+ {!confirmMode && (
+
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+
+ )}
);
}
diff --git a/components/Switch.tsx b/components/Switch.tsx
new file mode 100644
index 0000000000..d86538e159
--- /dev/null
+++ b/components/Switch.tsx
@@ -0,0 +1,117 @@
+import React, { ReactElement, ReactNode } from 'react';
+import styled from 'styled-components';
+import { size3, size4, size8 } from '../styles/sizes';
+import { colorWater50, colorWater60, colorWater90 } from '../styles/colors';
+import { typoNuggets } from '../styles/typography';
+
+const switchHeight = size4;
+
+// TODO: track height doesn't follow the sizing guideline
+const SwitchTrack = styled.span`
+ bottom: 0;
+ width: 100%;
+ height: 0.625rem;
+ margin: auto 0;
+ background: var(--theme-active);
+ will-change: background-color, opacity;
+ transition: background-color 0.1s linear, opacity 0.2s linear;
+`;
+
+const SwitchKnob = styled.span`
+ width: ${switchHeight};
+ height: ${switchHeight};
+ background: var(--theme-secondary);
+ will-change: transform, background-color;
+ transition: background-color 0.1s linear, transform 0.2s linear;
+`;
+
+// TODO: border-radius doesn't follow the sizing guideline
+const SwitchContainer = styled.span`
+ position: relative;
+ display: block;
+ width: ${size8};
+ height: ${switchHeight};
+
+ ${SwitchKnob}, ${SwitchTrack} {
+ position: absolute;
+ left: 0;
+ top: 0;
+ border-radius: 0.188rem;
+ }
+`;
+
+const Children = styled.span`
+ margin-left: ${size3};
+ color: var(--theme-secondary);
+ ${typoNuggets}
+`;
+
+// TODO: add support for light theme for the water colors
+const Container = styled.label`
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ &:hover ${SwitchKnob} {
+ background: var(--theme-primary);
+ }
+
+ &:hover input:checked ~ ${SwitchContainer} ${SwitchKnob} {
+ background: ${colorWater50};
+ }
+
+ &:active {
+ background: none;
+ }
+
+ input {
+ display: none;
+
+ &:checked {
+ & ~ ${SwitchContainer} ${SwitchTrack} {
+ background: ${colorWater90};
+ }
+
+ & ~ ${SwitchContainer} ${SwitchKnob} {
+ transform: translateX(100%);
+ background: ${colorWater60};
+ }
+
+ & ~ ${Children} {
+ color: var(--theme-primary);
+ }
+ }
+ }
+`;
+
+export interface Props {
+ children?: ReactNode;
+ className?: string;
+ inputId: string;
+ name: string;
+ checked?: boolean;
+}
+
+export default function Switch({
+ className,
+ inputId,
+ name,
+ checked,
+ children,
+}: Props): ReactElement {
+ return (
+
+
+
+
+
+
+ {children && {children}}
+
+ );
+}
diff --git a/components/utilities.tsx b/components/utilities.tsx
index 4ee1ec6128..76a556f1b0 100644
--- a/components/utilities.tsx
+++ b/components/utilities.tsx
@@ -2,7 +2,12 @@ import styled from 'styled-components';
import Linkify from 'linkifyjs/react';
import LazyImage from './LazyImage';
import { size10, size2, size3, size4, size6 } from '../styles/sizes';
-import { typoLil1, typoLil2Base, typoSmall } from '../styles/typography';
+import {
+ typoLil1,
+ typoLil2Base,
+ typoMicro2Base,
+ typoSmall,
+} from '../styles/typography';
import Loader from './Loader';
import { colorWater60 } from '../styles/colors';
@@ -53,3 +58,19 @@ export const ButtonLoader = styled(Loader)`
bottom: 0;
margin: auto;
`;
+
+export const LegalNotice = styled.div`
+ color: var(--theme-disabled);
+ text-align: center;
+ font-weight: bold;
+ ${typoMicro2Base};
+
+ a {
+ display: inline-block;
+ text-decoration: underline;
+ color: inherit;
+ @supports (display: contents) {
+ display: contents;
+ }
+ }
+`;
diff --git a/lib/constants.ts b/lib/constants.ts
new file mode 100644
index 0000000000..c0b9c623d3
--- /dev/null
+++ b/lib/constants.ts
@@ -0,0 +1,2 @@
+export const termsOfService = 'https://daily.dev/tos';
+export const privacyPolicy = 'https://daily.dev/privacy';
diff --git a/lib/user.ts b/lib/user.ts
index 3ff164e498..0a9c084da4 100644
--- a/lib/user.ts
+++ b/lib/user.ts
@@ -16,6 +16,7 @@ export interface LoggedUser {
providers: string[];
company?: string;
title?: string;
+ acceptedMarketing?: boolean;
}
export interface UserProfile {
@@ -60,6 +61,13 @@ export async function authenticate({
});
}
+export async function logout(): Promise {
+ await fetch(`${apiUrl}/v1/users/logout`, {
+ method: 'POST',
+ credentials: 'include',
+ });
+}
+
export async function updateProfile(
profile: UserProfile,
): Promise {
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 02c4a2358e..653f766026 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -11,15 +11,10 @@ import Seo from '../next-seo';
import { useApollo } from '../lib/apolloClient';
import GlobalStyle from '../components/GlobalStyle';
import AuthContext from '../components/AuthContext';
-import { LoggedUser } from '../lib/user';
+import { LoggedUser, logout as dispatchLogout } from '../lib/user';
-const LoginModal = dynamic(() => import('../components/LoginModal'), {
- ssr: false,
-});
-const ConfirmAccountModal = dynamic(
- () => import('../components/ConfirmAccountModal'),
- { ssr: false },
-);
+const LoginModal = dynamic(() => import('../components/LoginModal'));
+const ProfileModal = dynamic(() => import('../components/ProfileModal'));
interface PageProps {
user?: LoggedUser;
@@ -37,15 +32,24 @@ export default function App({
const apolloClient = useApollo(pageProps.initialApolloState);
const [user, setUser] = useState(pageProps.user);
const [loginIsOpen, setLoginIsOpen] = useState(false);
- const [confirmAccountIsOpen, setConfirmAccountIsOpen] = useState(
+ const [confirmAccount, setConfirmAccount] = useState(
pageProps.user?.providers && !pageProps.user.infoConfirmed,
);
+ const [profileIsOpen, setProfileIsOpen] = useState(confirmAccount);
const closeLogin = () => setLoginIsOpen(false);
- const closeConfirmAccount = () => setConfirmAccountIsOpen(false);
+ const closeConfirmAccount = () => setProfileIsOpen(false);
const profileUpdated = (newProfile: LoggedUser) => {
setUser({ ...user, ...newProfile });
- setConfirmAccountIsOpen(false);
+ setProfileIsOpen(false);
+ if (confirmAccount) {
+ setConfirmAccount(false);
+ }
+ };
+
+ const logout = async (): Promise => {
+ await dispatchLogout();
+ location.reload();
};
useEffect(() => {
@@ -66,6 +70,8 @@ export default function App({
user,
shouldShowLogin: loginIsOpen,
showLogin: () => setLoginIsOpen(true),
+ showProfile: () => setProfileIsOpen(true),
+ logout,
}}
>
@@ -102,10 +108,13 @@ export default function App({
contentLabel="Login Modal"
/>
{user && (
-
diff --git a/pages/posts/[id].tsx b/pages/posts/[id].tsx
index 7577c55b3a..60a9618cbe 100644
--- a/pages/posts/[id].tsx
+++ b/pages/posts/[id].tsx
@@ -61,14 +61,12 @@ import { colorPepper90 } from '../../styles/colors';
import { focusOutline, postPageMaxWidth } from '../../styles/utilities';
import { NextSeoProps } from 'next-seo/lib/types';
-const NewCommentModal = dynamic(
- () => import('../../components/NewCommentModal'),
- { ssr: false },
+const NewCommentModal = dynamic(() =>
+ import('../../components/NewCommentModal'),
);
-const DeleteCommentModal = dynamic(
- () => import('../../components/DeleteCommentModal'),
- { ssr: false },
+const DeleteCommentModal = dynamic(() =>
+ import('../../components/DeleteCommentModal'),
);
const ShareBar = dynamic(() => import('../../components/ShareBar'), {